keycloak-memoizeit

Changes

model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/ClientTemplateQuery.java 11(+0 -11)

model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/ClientQueryPredicate.java 48(+0 -48)

model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/ClientTemplateQueryPredicate.java 40(+0 -40)

model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/GroupQueryPredicate.java 40(+0 -40)

model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/RealmQueryPredicate.java 40(+0 -40)

model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/RoleQueryPredicate.java 40(+0 -40)

pom.xml 13(+6 -7)

README.md 2(+1 -1)

testsuite/integration-arquillian/tests/other/adapters/jboss/eap/src/test/java/org/keycloak/testsuite/adapter/EAPOIDCKerberosLdapAdapterTest.java 13(+0 -13)

testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/EAP6OIDCKerberosLdapAdapterTest.java 13(+0 -13)

testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/WildflyOIDCKerberosLdapAdapterTest.java 13(+0 -13)

Details

diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AuthenticatedActionsHandler.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AuthenticatedActionsHandler.java
index 121adf1..472afb7 100755
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AuthenticatedActionsHandler.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AuthenticatedActionsHandler.java
@@ -142,9 +142,13 @@ public class AuthenticatedActionsHandler {
             AuthorizationContext authorizationContext = policyEnforcer.enforce(facade);
             RefreshableKeycloakSecurityContext session = (RefreshableKeycloakSecurityContext) facade.getSecurityContext();
 
-            session.setAuthorizationContext(authorizationContext);
+            if (session != null) {
+                session.setAuthorizationContext(authorizationContext);
 
-            return  authorizationContext.isGranted();
+                return authorizationContext.isGranted();
+            }
+
+            return true;
         } catch (Exception e) {
             throw new RuntimeException("Failed to enforce policy decisions.", e);
         }
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java
index 9377b0b..0186e18 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java
@@ -19,6 +19,7 @@ package org.keycloak.adapters.authorization;
 
 import org.jboss.logging.Logger;
 import org.keycloak.AuthorizationContext;
+import org.keycloak.KeycloakSecurityContext;
 import org.keycloak.adapters.OIDCHttpFacade;
 import org.keycloak.adapters.spi.HttpFacade.Request;
 import org.keycloak.adapters.spi.HttpFacade.Response;
@@ -66,40 +67,51 @@ public abstract class AbstractPolicyEnforcer {
             return createEmptyAuthorizationContext(true);
         }
 
-        AccessToken accessToken = httpFacade.getSecurityContext().getToken();
-        Request request = httpFacade.getRequest();
-        Response response = httpFacade.getResponse();
-        String pathInfo = URI.create(request.getURI()).getPath().substring(1);
-        String path = pathInfo.substring(pathInfo.indexOf('/'), pathInfo.length());
-        PathConfig pathConfig = this.pathMatcher.matches(path, this.paths);
+        KeycloakSecurityContext securityContext = httpFacade.getSecurityContext();
 
-        LOGGER.debugf("Checking permissions for path [%s] with config [%s].", request.getURI(), pathConfig);
+        if (securityContext != null) {
+            AccessToken accessToken = securityContext.getToken();
 
-        if (pathConfig == null) {
-            if (EnforcementMode.PERMISSIVE.equals(enforcementMode)) {
-                return createAuthorizationContext(accessToken);
-            }
+            if (accessToken != null) {
+                Request request = httpFacade.getRequest();
+                Response response = httpFacade.getResponse();
+                String pathInfo = URI.create(request.getURI()).getPath().substring(1);
+                String path = pathInfo.substring(pathInfo.indexOf('/'), pathInfo.length());
+                PathConfig pathConfig = this.pathMatcher.matches(path, this.paths);
 
-            LOGGER.debugf("Could not find a configuration for path [%s]", path);
-            response.sendError(403, "Could not find a configuration for path [" + path + "].");
+                LOGGER.debugf("Checking permissions for path [%s] with config [%s].", request.getURI(), pathConfig);
 
-            return createEmptyAuthorizationContext(false);
-        }
+                if (pathConfig == null) {
+                    if (EnforcementMode.PERMISSIVE.equals(enforcementMode)) {
+                        return createAuthorizationContext(accessToken);
+                    }
 
-        PathConfig actualPathConfig = resolvePathConfig(pathConfig, request);
-        Set<String> requiredScopes = getRequiredScopes(actualPathConfig, request);
+                    LOGGER.debugf("Could not find a configuration for path [%s]", path);
+                    response.sendError(403, "Could not find a configuration for path [" + path + "].");
 
-        if (isAuthorized(actualPathConfig, requiredScopes, accessToken, httpFacade)) {
-            try {
-                return createAuthorizationContext(accessToken);
-            } catch (Exception e) {
-                throw new RuntimeException("Error processing path [" + actualPathConfig.getPath() + "].", e);
-            }
-        }
+                    return createEmptyAuthorizationContext(false);
+                }
+
+                if (EnforcementMode.DISABLED.equals(pathConfig.getEnforcementMode())) {
+                    return createEmptyAuthorizationContext(true);
+                }
+
+                PathConfig actualPathConfig = resolvePathConfig(pathConfig, request);
+                Set<String> requiredScopes = getRequiredScopes(actualPathConfig, request);
 
-        if (!challenge(actualPathConfig, requiredScopes, httpFacade)) {
-            LOGGER.debugf("Sending challenge to the client. Path [%s]", pathConfig);
-            response.sendError(403, "Authorization failed.");
+                if (isAuthorized(actualPathConfig, requiredScopes, accessToken, httpFacade)) {
+                    try {
+                        return createAuthorizationContext(accessToken);
+                    } catch (Exception e) {
+                        throw new RuntimeException("Error processing path [" + actualPathConfig.getPath() + "].", e);
+                    }
+                }
+
+                if (!challenge(actualPathConfig, requiredScopes, httpFacade)) {
+                    LOGGER.debugf("Sending challenge to the client. Path [%s]", pathConfig);
+                    response.sendError(403, "Authorization failed.");
+                }
+            }
         }
 
         return createEmptyAuthorizationContext(false);
@@ -125,14 +137,17 @@ public abstract class AbstractPolicyEnforcer {
         }
 
         List<Permission> permissions = authorization.getPermissions();
+        boolean hasPermission = false;
 
         for (Permission permission : permissions) {
             if (permission.getResourceSetId() != null) {
                 if (isResourcePermission(actualPathConfig, permission)) {
+                    hasPermission = true;
+
                     if (actualPathConfig.isInstance() && !matchResourcePermission(actualPathConfig, permission)) {
                         continue;
-
                     }
+
                     if (hasResourceScopePermission(requiredScopes, permission, actualPathConfig)) {
                         LOGGER.debugf("Authorization GRANTED for path [%s]. Permissions [%s].", actualPathConfig, permissions);
                         if (request.getMethod().equalsIgnoreCase("DELETE") && actualPathConfig.isInstance()) {
@@ -143,11 +158,16 @@ public abstract class AbstractPolicyEnforcer {
                 }
             } else {
                 if (hasResourceScopePermission(requiredScopes, permission, actualPathConfig)) {
+                    hasPermission = true;
                     return true;
                 }
             }
         }
 
+        if (!hasPermission && EnforcementMode.PERMISSIVE.equals(actualPathConfig.getEnforcementMode())) {
+            return true;
+        }
+
         LOGGER.debugf("Authorization FAILED for path [%s]. No enough permissions [%s].", actualPathConfig, permissions);
 
         return false;
@@ -218,6 +238,7 @@ public abstract class AbstractPolicyEnforcer {
                 config.setScopes(originalConfig.getScopes());
                 config.setMethods(originalConfig.getMethods());
                 config.setParentConfig(originalConfig);
+                config.setEnforcementMode(originalConfig.getEnforcementMode());
 
                 this.paths.add(config);
 
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java
index ff694bf..37b8f3d 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java
@@ -105,7 +105,16 @@ public class PolicyEnforcer {
     }
 
     private List<PathConfig> configurePaths(ProtectedResource protectedResource, PolicyEnforcerConfig enforcerConfig) {
-        if (enforcerConfig.getPaths().isEmpty()) {
+        boolean loadPathsFromServer = true;
+
+        for (PathConfig pathConfig : enforcerConfig.getPaths()) {
+            if (!PolicyEnforcerConfig.EnforcementMode.DISABLED.equals(pathConfig.getEnforcementMode())) {
+                loadPathsFromServer = false;
+                break;
+            }
+        }
+
+        if (loadPathsFromServer) {
             LOGGER.info("No path provided in configuration.");
             return configureAllPathsForResourceServer(protectedResource);
         } else {
diff --git a/adapters/saml/core-public/src/main/java/org/keycloak/adapters/saml/SamlPrincipal.java b/adapters/saml/core-public/src/main/java/org/keycloak/adapters/saml/SamlPrincipal.java
index e516e34..f9a1e77 100755
--- a/adapters/saml/core-public/src/main/java/org/keycloak/adapters/saml/SamlPrincipal.java
+++ b/adapters/saml/core-public/src/main/java/org/keycloak/adapters/saml/SamlPrincipal.java
@@ -105,7 +105,7 @@ public class SamlPrincipal implements Serializable, Principal {
      * @return
      */
     public List<String> getFriendlyAttributes(String friendlyName) {
-        List<String> list = friendlyAttributes.get(name);
+        List<String> list = friendlyAttributes.get(friendlyName);
         if (list != null) {
             return Collections.unmodifiableList(list);
         } else {
diff --git a/authz/client/pom.xml b/authz/client/pom.xml
index bf3f6b8..d053a99 100644
--- a/authz/client/pom.xml
+++ b/authz/client/pom.xml
@@ -18,6 +18,8 @@
     <description>KeyCloak AuthZ: Client API</description>
 
     <properties>
+        <maven.compiler.source>1.7</maven.compiler.source>
+        <maven.compiler.target>1.7</maven.compiler.target>
         <keycloak.osgi.export>
             org.keycloak.authorization.client.*
         </keycloak.osgi.export>
@@ -63,6 +65,14 @@
 
     <build>
         <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <configuration>
+		    <source>${maven.compiler.source}</source>
+		    <target>${maven.compiler.target}</target>
+                </configuration>
+            </plugin>
             <!-- Adding OSGI metadata to the JAR without changing the packaging type. -->
             <plugin>
                 <artifactId>maven-jar-plugin</artifactId>
@@ -98,4 +108,4 @@
         </plugins>
     </build>
 
-</project>
\ No newline at end of file
+</project>
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethod.java b/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethod.java
index be83987..d132620 100644
--- a/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethod.java
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethod.java
@@ -48,7 +48,7 @@ public class HttpMethod<R> {
     private HttpMethodResponse<R> response;
 
     public HttpMethod(Configuration configuration, RequestBuilder builder) {
-        this(configuration, builder, new HashMap<>(), new HashMap<>());
+        this(configuration, builder, new HashMap<String, String>(), new HashMap<String, String>());
     }
 
     public HttpMethod(Configuration configuration, RequestBuilder builder, HashMap<String, String> params, HashMap<String, String> headers) {
@@ -155,4 +155,4 @@ public class HttpMethod<R> {
             }
         };
     }
-}
\ No newline at end of file
+}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodResponse.java b/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodResponse.java
index 4155240..fceca19 100644
--- a/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodResponse.java
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodResponse.java
@@ -41,7 +41,7 @@ public class HttpMethodResponse<R> {
         });
     }
 
-    public HttpMethodResponse<R> json(Class<R> responseType) {
+    public HttpMethodResponse<R> json(final Class<R> responseType) {
         return new HttpMethodResponse<R>(this.method) {
             @Override
             public R execute() {
diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProviderFactory.java
index 301bb7b..67de87a 100644
--- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProviderFactory.java
+++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProviderFactory.java
@@ -45,7 +45,7 @@ public class RolePolicyProviderFactory implements PolicyProviderFactory {
 
     @Override
     public String getName() {
-        return "Role-Based";
+        return "Role";
     }
 
     @Override
diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/user/UserPolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/user/UserPolicyProviderFactory.java
index 9461474..fdeeac0 100644
--- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/user/UserPolicyProviderFactory.java
+++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/user/UserPolicyProviderFactory.java
@@ -43,7 +43,7 @@ public class UserPolicyProviderFactory implements PolicyProviderFactory {
 
     @Override
     public String getName() {
-        return "User-Based";
+        return "User";
     }
 
     @Override
diff --git a/authz/policy/drools/src/main/java/org/keycloak/authorization/policy/provider/drools/DroolsPolicyProviderFactory.java b/authz/policy/drools/src/main/java/org/keycloak/authorization/policy/provider/drools/DroolsPolicyProviderFactory.java
index 0e1e208..b3305ae 100644
--- a/authz/policy/drools/src/main/java/org/keycloak/authorization/policy/provider/drools/DroolsPolicyProviderFactory.java
+++ b/authz/policy/drools/src/main/java/org/keycloak/authorization/policy/provider/drools/DroolsPolicyProviderFactory.java
@@ -30,7 +30,7 @@ public class DroolsPolicyProviderFactory implements PolicyProviderFactory {
 
     @Override
     public String getName() {
-        return "Drools";
+        return "Rule";
     }
 
     @Override
diff --git a/core/src/main/java/org/keycloak/AuthorizationContext.java b/core/src/main/java/org/keycloak/AuthorizationContext.java
index 05bb97d..a14594b 100644
--- a/core/src/main/java/org/keycloak/AuthorizationContext.java
+++ b/core/src/main/java/org/keycloak/AuthorizationContext.java
@@ -18,9 +18,11 @@
 package org.keycloak;
 
 import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.AccessToken.Authorization;
 import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
 import org.keycloak.representations.idm.authorization.Permission;
 
+import java.util.Collections;
 import java.util.List;
 
 /**
@@ -44,7 +46,17 @@ public class AuthorizationContext {
     }
 
     public boolean hasPermission(String resourceName, String scopeName) {
-        for (Permission permission : authzToken.getAuthorization().getPermissions()) {
+        if (this.authzToken == null) {
+            return false;
+        }
+
+        Authorization authorization = this.authzToken.getAuthorization();
+
+        if (authorization == null) {
+            return false;
+        }
+
+        for (Permission permission : authorization.getPermissions()) {
             for (PathConfig pathHolder : this.paths) {
                 if (pathHolder.getName().equals(resourceName)) {
                     if (pathHolder.getId().equals(permission.getResourceSetId())) {
@@ -60,7 +72,17 @@ public class AuthorizationContext {
     }
 
     public boolean hasResourcePermission(String resourceName) {
-        for (Permission permission : authzToken.getAuthorization().getPermissions()) {
+        if (this.authzToken == null) {
+            return false;
+        }
+
+        Authorization authorization = this.authzToken.getAuthorization();
+
+        if (authorization == null) {
+            return false;
+        }
+
+        for (Permission permission : authorization.getPermissions()) {
             for (PathConfig pathHolder : this.paths) {
                 if (pathHolder.getName().equals(resourceName)) {
                     if (pathHolder.getId().equals(permission.getResourceSetId())) {
@@ -74,7 +96,17 @@ public class AuthorizationContext {
     }
 
     public boolean hasScopePermission(String scopeName) {
-        for (Permission permission : authzToken.getAuthorization().getPermissions()) {
+        if (this.authzToken == null) {
+            return false;
+        }
+
+        Authorization authorization = this.authzToken.getAuthorization();
+
+        if (authorization == null) {
+            return false;
+        }
+
+        for (Permission permission : authorization.getPermissions()) {
             if (permission.getScopes().contains(scopeName)) {
                 return true;
             }
@@ -84,7 +116,17 @@ public class AuthorizationContext {
     }
 
     public List<Permission> getPermissions() {
-        return this.authzToken.getAuthorization().getPermissions();
+        if (this.authzToken == null) {
+            return Collections.emptyList();
+        }
+
+        Authorization authorization = this.authzToken.getAuthorization();
+
+        if (authorization == null) {
+            return Collections.emptyList();
+        }
+
+        return Collections.unmodifiableList(authorization.getPermissions());
     }
 
     public boolean isGranted() {
diff --git a/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java b/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java
index 0c3faf8..db874c0 100644
--- a/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java
+++ b/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java
@@ -122,6 +122,9 @@ public class PolicyEnforcerConfig {
         private List<String> scopes = Collections.emptyList();
         private String id;
 
+        @JsonProperty("enforcement-mode")
+        private EnforcementMode enforcementMode = EnforcementMode.ENFORCING;
+
         @JsonIgnore
         private PathConfig parentConfig;
 
@@ -173,6 +176,14 @@ public class PolicyEnforcerConfig {
             return id;
         }
 
+        public EnforcementMode getEnforcementMode() {
+            return enforcementMode;
+        }
+
+        public void setEnforcementMode(EnforcementMode enforcementMode) {
+            this.enforcementMode = enforcementMode;
+        }
+
         @Override
         public String toString() {
             return "PathConfig{" +
@@ -181,6 +192,7 @@ public class PolicyEnforcerConfig {
                     ", path='" + path + '\'' +
                     ", scopes=" + scopes +
                     ", id='" + id + '\'' +
+                    ", enforcerMode='" + enforcementMode + '\'' +
                     '}';
         }
 
diff --git a/dependencies/server-all/pom.xml b/dependencies/server-all/pom.xml
index 526a0cf..3fdf489 100755
--- a/dependencies/server-all/pom.xml
+++ b/dependencies/server-all/pom.xml
@@ -256,6 +256,10 @@
             </exclusions>
         </dependency>
         <dependency>
+            <groupId>org.sonatype.sisu.inject</groupId>
+            <artifactId>guice-servlet</artifactId>
+        </dependency>
+        <dependency>
             <groupId>org.sonatype.plexus</groupId>
             <artifactId>plexus-cipher</artifactId>
         </dependency>
diff --git a/distribution/adapters/as7-eap6-adapter/as7-adapter-zip/assembly.xml b/distribution/adapters/as7-eap6-adapter/as7-adapter-zip/assembly.xml
index 15c4b6a..7419fa2 100755
--- a/distribution/adapters/as7-eap6-adapter/as7-adapter-zip/assembly.xml
+++ b/distribution/adapters/as7-eap6-adapter/as7-adapter-zip/assembly.xml
@@ -1,3 +1,4 @@
+
 <!--
   ~ Copyright 2016 Red Hat, Inc. and/or its affiliates
   ~ and other contributors as indicated by the @author tags.
@@ -39,6 +40,9 @@
                 <include>org/keycloak/keycloak-as7-subsystem/**</include>
                 <include>org/keycloak/keycloak-adapter-subsystem/**</include>
                 <include>org/keycloak/keycloak-servlet-oauth-client/**</include>
+
+                <!-- Authorization -->
+                <include>org/keycloak/keycloak-authz-client/**</include>
             </includes>
             <excludes>
                 <exclude>**/*.war</exclude>
diff --git a/distribution/adapters/as7-eap6-adapter/as7-modules/build.xml b/distribution/adapters/as7-eap6-adapter/as7-modules/build.xml
index 829a4d6..d22273f 100755
--- a/distribution/adapters/as7-eap6-adapter/as7-modules/build.xml
+++ b/distribution/adapters/as7-eap6-adapter/as7-modules/build.xml
@@ -91,6 +91,10 @@
             <maven-resource group="org.keycloak" artifact="keycloak-servlet-oauth-client"/>
         </module-def>
 
+        <!-- Authorization -->
+        <module-def name="org.keycloak.keycloak-authz-client">
+            <maven-resource group="org.keycloak" artifact="keycloak-authz-client"/>
+        </module-def>
     </target>
 
     <target name="clean-target">
diff --git a/distribution/adapters/as7-eap6-adapter/as7-modules/pom.xml b/distribution/adapters/as7-eap6-adapter/as7-modules/pom.xml
index 732e7c7..e484d8d 100755
--- a/distribution/adapters/as7-eap6-adapter/as7-modules/pom.xml
+++ b/distribution/adapters/as7-eap6-adapter/as7-modules/pom.xml
@@ -102,6 +102,11 @@
             <groupId>com.fasterxml.jackson.jaxrs</groupId>
             <artifactId>jackson-jaxrs-json-provider</artifactId>
         </dependency>
+        <!-- Authorization -->
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-authz-client</artifactId>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/distribution/adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-adapter-core/main/module.xml b/distribution/adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-adapter-core/main/module.xml
index 984cb50..21ea5ed 100755
--- a/distribution/adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-adapter-core/main/module.xml
+++ b/distribution/adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-adapter-core/main/module.xml
@@ -34,6 +34,7 @@
         <module name="org.keycloak.keycloak-common"/>
         <module name="org.keycloak.keycloak-core"/>
         <module name="org.keycloak.keycloak-adapter-spi"/>
+        <module name="org.keycloak.keycloak-authz-client"/>
     </dependencies>
 
 </module>
diff --git a/distribution/adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-authz-client/main/module.xml b/distribution/adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-authz-client/main/module.xml
new file mode 100644
index 0000000..3cd1abd
--- /dev/null
+++ b/distribution/adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-authz-client/main/module.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+
+
+<!--
+  ~ JBoss, Home of Professional Open Source.
+  ~ Copyright 2016 Red Hat, Inc., and individual contributors
+  ~ as indicated by the @author tags.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<module xmlns="urn:jboss:module:1.1" name="org.keycloak.keycloak-authz-client">
+    <resources>
+        <!-- Insert resources here -->
+    </resources>
+    <dependencies>
+        <module name="org.bouncycastle" />
+        <module name="javax.api"/>
+        <module name="javax.activation.api"/>
+        <module name="sun.jdk" optional="true" />
+        <module name="javax.ws.rs.api"/>
+        <module name="org.keycloak.keycloak-core"/>
+        <module name="org.keycloak.keycloak-common"/>
+        <module name="org.apache.httpcomponents"/>
+        <module name="com.fasterxml.jackson.core.jackson-core"/>
+        <module name="com.fasterxml.jackson.core.jackson-annotations"/>
+        <module name="com.fasterxml.jackson.core.jackson-databind"/>
+        <module name="com.fasterxml.jackson.jaxrs.jackson-jaxrs-json-provider"/>
+    </dependencies>
+
+</module>
diff --git a/distribution/adapters/as7-eap6-adapter/eap6-adapter-zip/assembly.xml b/distribution/adapters/as7-eap6-adapter/eap6-adapter-zip/assembly.xml
index c69ea6b..1fafc63 100755
--- a/distribution/adapters/as7-eap6-adapter/eap6-adapter-zip/assembly.xml
+++ b/distribution/adapters/as7-eap6-adapter/eap6-adapter-zip/assembly.xml
@@ -39,6 +39,9 @@
                 <include>org/keycloak/keycloak-as7-subsystem/**</include>
                 <include>org/keycloak/keycloak-adapter-subsystem/**</include>
                 <include>org/keycloak/keycloak-servlet-oauth-client/**</include>
+
+                <!-- Authorization -->
+                <include>org/keycloak/keycloak-authz-client/**</include>
             </includes>
             <excludes>
                 <exclude>**/*.war</exclude>
diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/eclipse/sisu/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/eclipse/sisu/main/module.xml
index 3861d46..55b92dc 100755
--- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/eclipse/sisu/main/module.xml
+++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/eclipse/sisu/main/module.xml
@@ -22,6 +22,7 @@
     <resources>
         <artifact name="${org.eclipse.sisu:org.eclipse.sisu.inject}"/>
         <artifact name="${org.eclipse.sisu:org.eclipse.sisu.plexus}"/>
+        <artifact name="${org.sonatype.sisu.inject:guice-servlet}"/>
     </resources>
     <dependencies>
         <module name="javax.api"/>
diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-infinispan/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-infinispan/main/module.xml
index a80a008..e7fdb8a 100755
--- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-infinispan/main/module.xml
+++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-infinispan/main/module.xml
@@ -30,6 +30,9 @@
         <module name="org.keycloak.keycloak-server-spi"/>
         <module name="org.keycloak.keycloak-server-spi-private"/>
         <module name="org.infinispan"/>
+        <module name="org.infinispan.commons"/>
+        <module name="org.infinispan.cachestore.remote"/>
+        <module name="org.infinispan.client.hotrod"/>
         <module name="org.jboss.logging"/>
         <module name="javax.api"/>
     </dependencies>
diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-services/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-services/main/module.xml
index 9db480c..cd71511 100755
--- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-services/main/module.xml
+++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-services/main/module.xml
@@ -30,7 +30,7 @@
         <module name="org.keycloak.keycloak-js-adapter" services="import"/>
         <module name="org.keycloak.keycloak-kerberos-federation" services="import"/>
         <module name="org.keycloak.keycloak-ldap-federation" services="import"/>
-        <module name="org.keycloak.keycloak-sssd-federation" services="import"/>
+        <module name="org.keycloak.keycloak-sssd-federation" optional="true" services="import"/>
         <module name="org.keycloak.keycloak-server-spi" services="import"/>
         <module name="org.keycloak.keycloak-server-spi-private" services="import"/>
         <module name="org.keycloak.keycloak-model-jpa" services="import"/>
diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-sssd-federation/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-sssd-federation/main/module.xml
index 58939eb..6d56d6e 100644
--- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-sssd-federation/main/module.xml
+++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-sssd-federation/main/module.xml
@@ -22,6 +22,7 @@
 
     <resources>
         <artifact name="${org.keycloak:keycloak-sssd-federation}"/>
+        <resource-root path="/usr/share/java/jna.jar"/>
     </resources>
 
     <dependencies>
diff --git a/distribution/server-overlay/src/main/cli/keycloak-install-ha.cli b/distribution/server-overlay/src/main/cli/keycloak-install-ha.cli
index 17fd5f0..a3b85f1 100644
--- a/distribution/server-overlay/src/main/cli/keycloak-install-ha.cli
+++ b/distribution/server-overlay/src/main/cli/keycloak-install-ha.cli
@@ -2,9 +2,9 @@ embed-server --server-config=standalone-ha.xml
 /subsystem=datasources/data-source=KeycloakDS/:add(connection-url="jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE",jta=false,driver-name=h2,jndi-name=java:jboss/datasources/KeycloakDS,password=sa,user-name=sa,use-java-context=true)
 /subsystem=infinispan/cache-container=keycloak:add(jndi-name="infinispan/Keycloak")
 /subsystem=infinispan/cache-container=keycloak/transport=TRANSPORT:add(lock-timeout=60000)
-/subsystem=infinispan/cache-container=keycloak/invalidation-cache=realms:add(mode="SYNC")
-/subsystem=infinispan/cache-container=keycloak/invalidation-cache=users:add(mode="SYNC")
-/subsystem=infinispan/cache-container=keycloak/invalidation-cache=users/eviction=EVICTION:add(max-entries=10000,strategy=LRU)
+/subsystem=infinispan/cache-container=keycloak/local-cache=realms:add()
+/subsystem=infinispan/cache-container=keycloak/local-cache=users:add()
+/subsystem=infinispan/cache-container=keycloak/local-cache=users/eviction=EVICTION:add(max-entries=10000,strategy=LRU)
 /subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions:add(mode="SYNC",owners="1")
 /subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineSessions:add(mode="SYNC",owners="1")
 /subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures:add(mode="SYNC",owners="1")
diff --git a/examples/providers/rest/README.md b/examples/providers/rest/README.md
index d78e9bc..9d26627 100644
--- a/examples/providers/rest/README.md
+++ b/examples/providers/rest/README.md
@@ -12,5 +12,5 @@ Then registering the provider by editing `standalone/configuration/standalone.xm
         <provider>module:org.keycloak.examples.hello-rest-example</provider>
     </providers>
 
-Then start (or restart) the server. Once started open http://localhost:8080/realms/master/hello and you should see the message _Hello master_.
-You can also invoke the endpoint for other realms by replacing `master` with the realm name in the above url.
\ No newline at end of file
+Then start (or restart) the server. Once started open http://localhost:8080/auth/realms/master/hello and you should see the message _Hello master_.
+You can also invoke the endpoint for other realms by replacing `master` with the realm name in the above url.
diff --git a/examples/saml/servlet-filter/src/main/webapp/META-INF/jboss-deployment-structure.xml b/examples/saml/servlet-filter/src/main/webapp/META-INF/jboss-deployment-structure.xml
new file mode 100644
index 0000000..b2ee966
--- /dev/null
+++ b/examples/saml/servlet-filter/src/main/webapp/META-INF/jboss-deployment-structure.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<jboss-deployment-structure>
+<deployment>
+    <dependencies>
+        <module name="org.apache.httpcomponents" />
+    </dependencies>
+</deployment>
+</jboss-deployment-structure>
\ No newline at end of file
diff --git a/federation/sssd/pom.xml b/federation/sssd/pom.xml
index 29113f7..a9029c4 100644
--- a/federation/sssd/pom.xml
+++ b/federation/sssd/pom.xml
@@ -49,6 +49,7 @@
         <dependency>
             <groupId>net.java.dev.jna</groupId>
             <artifactId>jna</artifactId>
+            <scope>provided</scope>
         </dependency>
         <dependency>
             <groupId>org.keycloak</groupId>
@@ -70,10 +71,6 @@
             <artifactId>jboss-logging</artifactId>
             <scope>provided</scope>
         </dependency>
-        <dependency>
-            <groupId>com.github.jnr</groupId>
-            <artifactId>jnr-unixsocket</artifactId>
-        </dependency>
     </dependencies>
 
 </project>
diff --git a/federation/sssd/src/main/java/cx/ath/matthew/LibraryLoader.java b/federation/sssd/src/main/java/cx/ath/matthew/LibraryLoader.java
new file mode 100644
index 0000000..4088d46
--- /dev/null
+++ b/federation/sssd/src/main/java/cx/ath/matthew/LibraryLoader.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package cx.ath.matthew;
+
+/**
+ * @author <a href="mailto:bruno@abstractj.org">Bruno Oliveira</a>.
+ */
+public class LibraryLoader {
+
+    private static final String[] PATHS = {"/usr/lib/", "/usr/lib64/", "/usr/local/lib/", "/opt/local/lib/"};
+    private static final String LIBRARY_NAME = "libunix_dbus_java";
+    private static final String VERSION = "0.0.8";
+    private static boolean loadSucceeded;
+
+    public static LibraryLoader load() {
+        for (String path : PATHS) {
+            try {
+                System.load(String.format("%s/%s.so.%s", path, LIBRARY_NAME, VERSION));
+                loadSucceeded = true;
+                break;
+            } catch (UnsatisfiedLinkError e) {
+                loadSucceeded = false;
+            }
+        }
+
+        return new LibraryLoader();
+    }
+
+    public boolean succeed() {
+        return loadSucceeded;
+    }
+}
diff --git a/federation/sssd/src/main/java/cx/ath/matthew/unix/UnixIOException.java b/federation/sssd/src/main/java/cx/ath/matthew/unix/UnixIOException.java
new file mode 100644
index 0000000..24fd20c
--- /dev/null
+++ b/federation/sssd/src/main/java/cx/ath/matthew/unix/UnixIOException.java
@@ -0,0 +1,43 @@
+/*
+ * Java Unix Sockets Library
+ *
+ * Copyright (c) Matthew Johnson 2004
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ * 
+ * To Contact the author, please email src@matthew.ath.cx
+ *
+ */
+package cx.ath.matthew.unix;
+
+import java.io.IOException;
+
+/**
+ * An IO Exception which occurred during UNIX Socket IO
+ */
+public class UnixIOException extends IOException {
+    private int no;
+    private String message;
+
+    public UnixIOException(int no, String message) {
+        super(message);
+        this.message = message;
+        this.no = no;
+    }
+}
diff --git a/federation/sssd/src/main/java/cx/ath/matthew/unix/UnixSocket.java b/federation/sssd/src/main/java/cx/ath/matthew/unix/UnixSocket.java
index 7537f01..8851637 100644
--- a/federation/sssd/src/main/java/cx/ath/matthew/unix/UnixSocket.java
+++ b/federation/sssd/src/main/java/cx/ath/matthew/unix/UnixSocket.java
@@ -26,11 +26,9 @@
  */
 package cx.ath.matthew.unix;
 
+import cx.ath.matthew.LibraryLoader;
 import cx.ath.matthew.debug.Debug;
-import jnr.unixsocket.UnixSocketAddress;
-import jnr.unixsocket.UnixSocketChannel;
 
-import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -39,8 +37,25 @@ import java.io.OutputStream;
  * Represents a UnixSocket.
  */
 public class UnixSocket {
+    static {
+        LibraryLoader.load();
+    }
+
+    private native void native_set_pass_cred(int sock, boolean passcred) throws IOException;
+
+    private native int native_connect(String address, boolean abs) throws IOException;
+
+    private native void native_close(int sock) throws IOException;
+
+    private native int native_getPID(int sock);
+
+    private native int native_getUID(int sock);
 
-    private UnixSocketChannel channel;
+    private native int native_getGID(int sock);
+
+    private native void native_send_creds(int sock, byte data) throws IOException;
+
+    private native byte native_recv_creds(int sock, int[] creds) throws IOException;
 
     private UnixSocketAddress address = null;
     private USOutputStream os = null;
@@ -58,8 +73,8 @@ public class UnixSocket {
         this.sock = sock;
         this.address = address;
         this.connected = true;
-        this.os = new USOutputStream(channel, sock, this);
-        this.is = new USInputStream(channel, this);
+        this.os = new USOutputStream(sock, this);
+        this.is = new USInputStream(sock, this);
     }
 
     /**
@@ -83,7 +98,7 @@ public class UnixSocket {
      * @param address The Unix Socket address to connect to
      */
     public UnixSocket(String address) throws IOException {
-        this(new UnixSocketAddress(new File(address)));
+        this(new UnixSocketAddress(address));
     }
 
     /**
@@ -93,11 +108,9 @@ public class UnixSocket {
      */
     public void connect(UnixSocketAddress address) throws IOException {
         if (connected) close();
-        this.channel = UnixSocketChannel.open(address);
-        this.channel = UnixSocketChannel.open(address);
-        this.sock = channel.getFD();
-        this.os = new USOutputStream(channel, sock, this);
-        this.is = new USInputStream(channel, this);
+        this.sock = native_connect(address.path, address.abs);
+        this.os = new USOutputStream(this.sock, this);
+        this.is = new USInputStream(this.sock, this);
         this.address = address;
         this.connected = true;
         this.closed = false;
@@ -110,7 +123,7 @@ public class UnixSocket {
      * @param address The Unix Socket address to connect to
      */
     public void connect(String address) throws IOException {
-        connect(new UnixSocketAddress(new File(address)));
+        connect(new UnixSocketAddress(address));
     }
 
     public void finalize() {
@@ -125,7 +138,7 @@ public class UnixSocket {
      */
     public synchronized void close() throws IOException {
         if (Debug.debug) Debug.print(Debug.INFO, "Closing socket");
-        channel.close();
+        native_close(sock);
         sock = 0;
         this.closed = true;
         this.connected = false;
@@ -169,7 +182,91 @@ public class UnixSocket {
      */
     public void sendCredentialByte(byte data) throws IOException {
         if (!connected) throw new NotConnectedException();
-            os.send(channel.getFD(), new byte[]{ data });
+        native_send_creds(sock, data);
+    }
+
+    /**
+     * Receive a single byte of data, with credentials.
+     * (Works on BSDs)
+     *
+     * @param data The byte of data to send.
+     * @see getPeerUID
+     * @see getPeerPID
+     * @see getPeerGID
+     */
+    public byte recvCredentialByte() throws IOException {
+        if (!connected) throw new NotConnectedException();
+        int[] creds = new int[]{-1, -1, -1};
+        byte data = native_recv_creds(sock, creds);
+        pid = creds[0];
+        uid = creds[1];
+        gid = creds[2];
+        return data;
+    }
+
+    /**
+     * Get the credential passing status.
+     * (only effective on linux)
+     *
+     * @return The current status of credential passing.
+     * @see setPassCred
+     */
+    public boolean getPassCred() {
+        return passcred;
+    }
+
+    /**
+     * Return the uid of the remote process.
+     * Some data must have been received on the socket to do this.
+     * Either setPassCred must be called on Linux first, or recvCredentialByte
+     * on BSD.
+     *
+     * @return the UID or -1 if it is not available
+     */
+    public int getPeerUID() {
+        if (-1 == uid)
+            uid = native_getUID(sock);
+        return uid;
+    }
+
+    /**
+     * Return the gid of the remote process.
+     * Some data must have been received on the socket to do this.
+     * Either setPassCred must be called on Linux first, or recvCredentialByte
+     * on BSD.
+     *
+     * @return the GID or -1 if it is not available
+     */
+    public int getPeerGID() {
+        if (-1 == gid)
+            gid = native_getGID(sock);
+        return gid;
+    }
+
+    /**
+     * Return the pid of the remote process.
+     * Some data must have been received on the socket to do this.
+     * Either setPassCred must be called on Linux first, or recvCredentialByte
+     * on BSD.
+     *
+     * @return the PID or -1 if it is not available
+     */
+    public int getPeerPID() {
+        if (-1 == pid)
+            pid = native_getPID(sock);
+        return pid;
+    }
+
+    /**
+     * Set the credential passing status.
+     * (Only does anything on linux, for other OS, you need
+     * to use send/recv credentials)
+     *
+     * @param enable Set to true for credentials to be passed.
+     */
+    public void setPassCred(boolean enable) throws IOException {
+        native_set_pass_cred(sock, enable);
+        passcred = enable;
     }
 
     /**
diff --git a/federation/sssd/src/main/java/cx/ath/matthew/unix/UnixSocketAddress.java b/federation/sssd/src/main/java/cx/ath/matthew/unix/UnixSocketAddress.java
new file mode 100644
index 0000000..0baba47
--- /dev/null
+++ b/federation/sssd/src/main/java/cx/ath/matthew/unix/UnixSocketAddress.java
@@ -0,0 +1,86 @@
+/*
+ * Java Unix Sockets Library
+ *
+ * Copyright (c) Matthew Johnson 2004
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ * 
+ * To Contact the author, please email src@matthew.ath.cx
+ *
+ */
+package cx.ath.matthew.unix;
+
+/**
+ * Represents an address for a Unix Socket
+ */
+public class UnixSocketAddress {
+    String path;
+    boolean abs;
+
+    /**
+     * Create the address.
+     *
+     * @param path The path to the Unix Socket.
+     * @param abs  True if this should be an abstract socket.
+     */
+    public UnixSocketAddress(String path, boolean abs) {
+        this.path = path;
+        this.abs = abs;
+    }
+
+    /**
+     * Create the address.
+     *
+     * @param path The path to the Unix Socket.
+     */
+    public UnixSocketAddress(String path) {
+        this.path = path;
+        this.abs = false;
+    }
+
+    /**
+     * Return the path.
+     */
+    public String getPath() {
+        return path;
+    }
+
+    /**
+     * Returns true if this an address for an abstract socket.
+     */
+    public boolean isAbstract() {
+        return abs;
+    }
+
+    /**
+     * Return the Address as a String.
+     */
+    public String toString() {
+        return "unix" + (abs ? ":abstract" : "") + ":path=" + path;
+    }
+
+    public boolean equals(Object o) {
+        if (!(o instanceof UnixSocketAddress)) return false;
+        return ((UnixSocketAddress) o).path.equals(this.path);
+    }
+
+    public int hashCode() {
+        return path.hashCode();
+    }
+}
diff --git a/federation/sssd/src/main/java/cx/ath/matthew/unix/USInputStream.java b/federation/sssd/src/main/java/cx/ath/matthew/unix/USInputStream.java
index b11609f..eb143fe 100644
--- a/federation/sssd/src/main/java/cx/ath/matthew/unix/USInputStream.java
+++ b/federation/sssd/src/main/java/cx/ath/matthew/unix/USInputStream.java
@@ -26,25 +26,25 @@
  */
 package cx.ath.matthew.unix;
 
-import jnr.unixsocket.UnixSocketChannel;
-
 import java.io.IOException;
 import java.io.InputStream;
-import java.nio.channels.Channels;
 
 public class USInputStream extends InputStream {
     public static final int MSG_DONTWAIT = 0x40;
-    private UnixSocketChannel channel;
 
+    private native int native_recv(int sock, byte[] b, int off, int len, int flags, int timeout) throws IOException;
+
+    private int sock;
     boolean closed = false;
     private byte[] onebuf = new byte[1];
     private UnixSocket us;
+    private boolean blocking = true;
     private int flags = 0;
     private int timeout = 0;
 
-    public USInputStream(UnixSocketChannel channel, UnixSocket us) {
+    public USInputStream(int sock, UnixSocket us) {
+        this.sock = sock;
         this.us = us;
-        this.channel = channel;
     }
 
     public void close() throws IOException {
@@ -65,8 +65,7 @@ public class USInputStream extends InputStream {
 
     public int read(byte[] b, int off, int len) throws IOException {
         if (closed) throw new NotConnectedException();
-        int count = receive(b, off, len);
-
+        int count = native_recv(sock, b, off, len, flags, timeout);
       /* Yes, I really want to do this. Recv returns 0 for 'connection shut down'.
        * read() returns -1 for 'end of stream.
        * Recv returns -1 for 'EAGAIN' (all other errors cause an exception to be raised)
@@ -92,21 +91,4 @@ public class USInputStream extends InputStream {
     public void setSoTimeout(int timeout) {
         this.timeout = timeout;
     }
-
-    /*
-     * Taken from JRuby with small modifications
-     * @see <a href="https://github.com/jruby/jruby/blob/master/core/src/main/java/org/jruby/ext/socket/RubyUNIXSocket.java">RubyUNIXSocket.java</a>
-     */
-    private int receive(byte[] dataBytes, int off, int len) {
-        int recvStatus = -1;
-        try {
-            InputStream inputStream = Channels.newInputStream(channel);
-            recvStatus = inputStream.read(dataBytes, off, len);
-
-        } catch (IOException e) {
-            e.printStackTrace();
-        }
-
-        return recvStatus;
-    }
 }
diff --git a/federation/sssd/src/main/java/cx/ath/matthew/unix/USOutputStream.java b/federation/sssd/src/main/java/cx/ath/matthew/unix/USOutputStream.java
index 1855b26..d8c85a7 100644
--- a/federation/sssd/src/main/java/cx/ath/matthew/unix/USOutputStream.java
+++ b/federation/sssd/src/main/java/cx/ath/matthew/unix/USOutputStream.java
@@ -26,31 +26,22 @@
  */
 package cx.ath.matthew.unix;
 
-import jnr.constants.platform.linux.SocketLevel;
-import jnr.posix.CmsgHdr;
-import jnr.posix.MsgHdr;
-import jnr.posix.POSIX;
-import jnr.posix.POSIXFactory;
-import jnr.unixsocket.UnixSocketChannel;
-
 import java.io.IOException;
 import java.io.OutputStream;
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
 
 public class USOutputStream extends OutputStream {
+    private native int native_send(int sock, byte[] b, int off, int len) throws IOException;
 
-    private UnixSocketChannel channel;
+    private native int native_send(int sock, byte[][] b) throws IOException;
 
     private int sock;
     boolean closed = false;
     private byte[] onebuf = new byte[1];
     private UnixSocket us;
 
-    public USOutputStream(UnixSocketChannel channel, int sock, UnixSocket us) {
+    public USOutputStream(int sock, UnixSocket us) {
         this.sock = sock;
         this.us = us;
-        this.channel = channel;
     }
 
     public void close() throws IOException {
@@ -61,9 +52,14 @@ public class USOutputStream extends OutputStream {
     public void flush() {
     } // no-op, we do not buffer
 
+    public void write(byte[][] b) throws IOException {
+        if (closed) throw new NotConnectedException();
+        native_send(sock, b);
+    }
+
     public void write(byte[] b, int off, int len) throws IOException {
         if (closed) throw new NotConnectedException();
-        send(sock, b, off, len);
+        native_send(sock, b, off, len);
     }
 
     public void write(int b) throws IOException {
@@ -79,46 +75,4 @@ public class USOutputStream extends OutputStream {
     public UnixSocket getSocket() {
         return us;
     }
-
-    /*
-     * Taken from JRuby with small modifications
-     * @see <a href="https://github.com/jruby/jruby/blob/master/core/src/main/java/org/jruby/ext/socket/RubyUNIXSocket.java">RubyUNIXSocket.java</a>
-     */
-    private void send(int sock, ByteBuffer[] outIov) {
-
-        final POSIX posix = POSIXFactory.getNativePOSIX();
-        MsgHdr outMessage = posix.allocateMsgHdr();
-
-        outMessage.setIov(outIov);
-
-        CmsgHdr outControl = outMessage.allocateControl(4);
-        outControl.setLevel(SocketLevel.SOL_SOCKET.intValue());
-        outControl.setType(0x01);
-
-        ByteBuffer fdBuf = ByteBuffer.allocateDirect(4);
-        fdBuf.order(ByteOrder.nativeOrder());
-        fdBuf.putInt(0, channel.getFD());
-        outControl.setData(fdBuf);
-
-        posix.sendmsg(sock, outMessage, 0);
-
-    }
-
-    private void send(int sock, byte[] dataBytes, int off, int len) {
-        ByteBuffer[] outIov = new ByteBuffer[1];
-        outIov[0] = ByteBuffer.allocateDirect(dataBytes.length);
-        outIov[0].put(dataBytes, off, len);
-        outIov[0].flip();
-
-        send(sock, outIov);
-    }
-
-    protected void send(int sock, byte[] dataBytes) {
-        ByteBuffer[] outIov = new ByteBuffer[1];
-        outIov[0] = ByteBuffer.allocateDirect(dataBytes.length);
-        outIov[0].put(dataBytes);
-        outIov[0].flip();
-
-        send(sock, outIov);
-    }
 }
diff --git a/federation/sssd/src/main/java/org/freedesktop/dbus/MessageWriter.java b/federation/sssd/src/main/java/org/freedesktop/dbus/MessageWriter.java
index 2a53426..45e8cb7 100644
--- a/federation/sssd/src/main/java/org/freedesktop/dbus/MessageWriter.java
+++ b/federation/sssd/src/main/java/org/freedesktop/dbus/MessageWriter.java
@@ -43,12 +43,20 @@ public class MessageWriter {
             if (Debug.debug) Debug.print(Debug.WARN, "Message " + m + " wire-data was null!");
             return;
         }
-        for (byte[] buf : m.getWireData()) {
-            if (Debug.debug)
-                Debug.print(Debug.VERBOSE, "(" + buf + "):" + (null == buf ? "" : Hexdump.format(buf)));
-            if (null == buf) break;
-            out.write(buf);
-        }
+        if (isunix) {
+            if (Debug.debug) {
+                Debug.print(Debug.DEBUG, "Writing all " + m.getWireData().length + " buffers simultaneously to Unix Socket");
+                for (byte[] buf : m.getWireData())
+                    Debug.print(Debug.VERBOSE, "(" + buf + "):" + (null == buf ? "" : Hexdump.format(buf)));
+            }
+            ((USOutputStream) out).write(m.getWireData());
+        } else
+            for (byte[] buf : m.getWireData()) {
+                if (Debug.debug)
+                    Debug.print(Debug.VERBOSE, "(" + buf + "):" + (null == buf ? "" : Hexdump.format(buf)));
+                if (null == buf) break;
+                out.write(buf);
+            }
         out.flush();
     }
 
diff --git a/federation/sssd/src/main/java/org/freedesktop/dbus/Transport.java b/federation/sssd/src/main/java/org/freedesktop/dbus/Transport.java
index 0c997bd..1745bcf 100644
--- a/federation/sssd/src/main/java/org/freedesktop/dbus/Transport.java
+++ b/federation/sssd/src/main/java/org/freedesktop/dbus/Transport.java
@@ -12,6 +12,7 @@ package org.freedesktop.dbus;
 
 import cx.ath.matthew.debug.Debug;
 import cx.ath.matthew.unix.UnixSocket;
+import cx.ath.matthew.unix.UnixSocketAddress;
 import cx.ath.matthew.utils.Hexdump;
 
 import java.io.BufferedReader;
@@ -25,6 +26,7 @@ import java.io.OutputStream;
 import java.io.PrintWriter;
 import java.lang.reflect.Method;
 import java.net.InetSocketAddress;
+import java.net.ServerSocket;
 import java.net.Socket;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
@@ -255,8 +257,10 @@ public class Transport {
             return new String(res);
         }
 
+        public static final int MODE_SERVER = 1;
         public static final int MODE_CLIENT = 2;
 
+        public static final int AUTH_NONE = 0;
         public static final int AUTH_EXTERNAL = 1;
         public static final int AUTH_SHA = 2;
         public static final int AUTH_ANON = 4;
@@ -273,12 +277,15 @@ public class Transport {
         public static final int WAIT_DATA = 1;
         public static final int WAIT_OK = 2;
         public static final int WAIT_REJECT = 3;
+        public static final int WAIT_AUTH = 4;
+        public static final int WAIT_BEGIN = 5;
         public static final int AUTHENTICATED = 6;
         public static final int FAILED = 7;
 
         public static final int OK = 1;
         public static final int CONTINUE = 2;
         public static final int ERROR = 3;
+        public static final int REJECT = 4;
 
         public Command receive(InputStream s) throws IOException {
             StringBuffer sb = new StringBuffer();
@@ -388,8 +395,89 @@ public class Transport {
             }
         }
 
+        public String challenge = "";
         public String cookie = "";
 
+        public int do_response(int auth, String Uid, String kernelUid, Command c) {
+            MessageDigest md = null;
+            try {
+                md = MessageDigest.getInstance("SHA");
+            } catch (NoSuchAlgorithmException NSAe) {
+                if (Debug.debug && AbstractConnection.EXCEPTION_DEBUG) Debug.print(Debug.ERR, NSAe);
+                return ERROR;
+            }
+            switch (auth) {
+                case AUTH_NONE:
+                    switch (c.getMechs()) {
+                        case AUTH_ANON:
+                            return OK;
+                        case AUTH_EXTERNAL:
+                            if (0 == col.compare(Uid, c.getData()) &&
+                                    (null == kernelUid || 0 == col.compare(Uid, kernelUid)))
+                                return OK;
+                            else
+                                return ERROR;
+                        case AUTH_SHA:
+                            String context = COOKIE_CONTEXT;
+                            long id = System.currentTimeMillis();
+                            byte[] buf = new byte[8];
+                            Message.marshallintBig(id, buf, 0, 8);
+                            challenge = stupidlyEncode(md.digest(buf));
+                            Random r = new Random();
+                            r.nextBytes(buf);
+                            cookie = stupidlyEncode(md.digest(buf));
+                            try {
+                                addCookie(context, "" + id, id / 1000, cookie);
+                            } catch (IOException IOe) {
+                                if (Debug.debug && AbstractConnection.EXCEPTION_DEBUG) Debug.print(Debug.ERR, IOe);
+                            }
+                            if (Debug.debug)
+                                Debug.print(Debug.DEBUG, "Sending challenge: " + context + ' ' + id + ' ' + challenge);
+                            c.setResponse(stupidlyEncode(context + ' ' + id + ' ' + challenge));
+                            return CONTINUE;
+                        default:
+                            return ERROR;
+                    }
+                case AUTH_SHA:
+                    String[] response = stupidlyDecode(c.getData()).split(" ");
+                    if (response.length < 2) return ERROR;
+                    String cchal = response[0];
+                    String hash = response[1];
+                    String prehash = challenge + ":" + cchal + ":" + cookie;
+                    byte[] buf = md.digest(prehash.getBytes());
+                    String posthash = stupidlyEncode(buf);
+                    if (Debug.debug)
+                        Debug.print(Debug.DEBUG, "Authenticating Hash; data=" + prehash + " remote hash=" + hash + " local hash=" + posthash);
+                    if (0 == col.compare(posthash, hash))
+                        return OK;
+                    else
+                        return ERROR;
+                default:
+                    return ERROR;
+            }
+        }
+
+        public String[] getTypes(int types) {
+            switch (types) {
+                case AUTH_EXTERNAL:
+                    return new String[]{"EXTERNAL"};
+                case AUTH_SHA:
+                    return new String[]{"DBUS_COOKIE_SHA1"};
+                case AUTH_ANON:
+                    return new String[]{"ANONYMOUS"};
+                case AUTH_SHA + AUTH_EXTERNAL:
+                    return new String[]{"EXTERNAL", "DBUS_COOKIE_SHA1"};
+                case AUTH_SHA + AUTH_ANON:
+                    return new String[]{"ANONYMOUS", "DBUS_COOKIE_SHA1"};
+                case AUTH_EXTERNAL + AUTH_ANON:
+                    return new String[]{"ANONYMOUS", "EXTERNAL"};
+                case AUTH_EXTERNAL + AUTH_ANON + AUTH_SHA:
+                    return new String[]{"ANONYMOUS", "EXTERNAL", "DBUS_COOKIE_SHA1"};
+                default:
+                    return new String[]{};
+            }
+        }
+
         /**
          * performs SASL auth on the given streams.
          * Mode selects whether to run as a SASL server or client.
@@ -400,6 +488,7 @@ public class Transport {
         public boolean auth(int mode, int types, String guid, OutputStream out, InputStream in, UnixSocket us) throws IOException {
             String username = System.getProperty("user.name");
             String Uid = null;
+            String kernelUid = null;
             try {
                 Class c = Class.forName("com.sun.security.auth.module.UnixSystem");
                 Method m = c.getMethod("getUid");
@@ -529,6 +618,110 @@ public class Transport {
                                 state = FAILED;
                         }
                         break;
+                    case MODE_SERVER:
+                        switch (state) {
+                            case INITIAL_STATE:
+                                byte[] buf = new byte[1];
+                                if (null == us) {
+                                    in.read(buf);
+                                } else {
+                                    buf[0] = us.recvCredentialByte();
+                                    int kuid = us.getPeerUID();
+                                    if (kuid >= 0)
+                                        kernelUid = stupidlyEncode("" + kuid);
+                                }
+                                if (0 != buf[0]) state = FAILED;
+                                else state = WAIT_AUTH;
+                                break;
+                            case WAIT_AUTH:
+                                c = receive(in);
+                                switch (c.getCommand()) {
+                                    case COMMAND_AUTH:
+                                        if (null == c.getData()) {
+                                            send(out, COMMAND_REJECTED, getTypes(types));
+                                        } else {
+                                            switch (do_response(current, Uid, kernelUid, c)) {
+                                                case CONTINUE:
+                                                    send(out, COMMAND_DATA, c.getResponse());
+                                                    current = c.getMechs();
+                                                    state = WAIT_DATA;
+                                                    break;
+                                                case OK:
+                                                    send(out, COMMAND_OK, guid);
+                                                    state = WAIT_BEGIN;
+                                                    current = 0;
+                                                    break;
+                                                case REJECT:
+                                                    send(out, COMMAND_REJECTED, getTypes(types));
+                                                    current = 0;
+                                                    break;
+                                            }
+                                        }
+                                        break;
+                                    case COMMAND_ERROR:
+                                        send(out, COMMAND_REJECTED, getTypes(types));
+                                        break;
+                                    case COMMAND_BEGIN:
+                                        state = FAILED;
+                                        break;
+                                    default:
+                                        send(out, COMMAND_ERROR, "Got invalid command");
+                                        break;
+                                }
+                                break;
+                            case WAIT_DATA:
+                                c = receive(in);
+                                switch (c.getCommand()) {
+                                    case COMMAND_DATA:
+                                        switch (do_response(current, Uid, kernelUid, c)) {
+                                            case CONTINUE:
+                                                send(out, COMMAND_DATA, c.getResponse());
+                                                state = WAIT_DATA;
+                                                break;
+                                            case OK:
+                                                send(out, COMMAND_OK, guid);
+                                                state = WAIT_BEGIN;
+                                                current = 0;
+                                                break;
+                                            case REJECT:
+                                                send(out, COMMAND_REJECTED, getTypes(types));
+                                                current = 0;
+                                                break;
+                                        }
+                                        break;
+                                    case COMMAND_ERROR:
+                                    case COMMAND_CANCEL:
+                                        send(out, COMMAND_REJECTED, getTypes(types));
+                                        state = WAIT_AUTH;
+                                        break;
+                                    case COMMAND_BEGIN:
+                                        state = FAILED;
+                                        break;
+                                    default:
+                                        send(out, COMMAND_ERROR, "Got invalid command");
+                                        break;
+                                }
+                                break;
+                            case WAIT_BEGIN:
+                                c = receive(in);
+                                switch (c.getCommand()) {
+                                    case COMMAND_ERROR:
+                                    case COMMAND_CANCEL:
+                                        send(out, COMMAND_REJECTED, getTypes(types));
+                                        state = WAIT_AUTH;
+                                        break;
+                                    case COMMAND_BEGIN:
+                                        state = AUTHENTICATED;
+                                        break;
+                                    default:
+                                        send(out, COMMAND_ERROR, "Got invalid command");
+                                        break;
+                                }
+                                break;
+                            default:
+                                state = FAILED;
+                        }
+                        break;
                     default:
                         return false;
                 }
@@ -588,15 +781,25 @@ public class Transport {
             types = SASL.AUTH_EXTERNAL;
             mode = SASL.MODE_CLIENT;
             us = new UnixSocket();
-            if (null != address.getParameter("path"))
-                us.connect(new jnr.unixsocket.UnixSocketAddress(new File(address.getParameter("path"))));
+            if (null != address.getParameter("abstract"))
+                us.connect(new UnixSocketAddress(address.getParameter("abstract"), true));
+            else if (null != address.getParameter("path"))
+                us.connect(new UnixSocketAddress(address.getParameter("path"), false));
+            us.setPassCred(true);
             in = us.getInputStream();
             out = us.getOutputStream();
         } else if ("tcp".equals(address.getType())) {
             types = SASL.AUTH_SHA;
-            mode = SASL.MODE_CLIENT;
-            s = new Socket();
-            s.connect(new InetSocketAddress(address.getParameter("host"), Integer.parseInt(address.getParameter("port"))));
+            if (null != address.getParameter("listen")) {
+                mode = SASL.MODE_SERVER;
+                ServerSocket ss = new ServerSocket();
+                ss.bind(new InetSocketAddress(address.getParameter("host"), Integer.parseInt(address.getParameter("port"))));
+                s = ss.accept();
+            } else {
+                mode = SASL.MODE_CLIENT;
+                s = new Socket();
+                s.connect(new InetSocketAddress(address.getParameter("host"), Integer.parseInt(address.getParameter("port"))));
+            }
             in = s.getInputStream();
             out = s.getOutputStream();
         } else {
diff --git a/misc/CrossDataCenter.md b/misc/CrossDataCenter.md
new file mode 100644
index 0000000..4146eaa
--- /dev/null
+++ b/misc/CrossDataCenter.md
@@ -0,0 +1,116 @@
+Test Cross-Data-Center scenario (test with external JDG server)
+===============================================================
+
+These are temporary notes. This docs should be removed once we have cross-DC support finished and properly documented. 
+
+What is working right now is:
+- Propagating of invalidation messages for "realms" and "users" caches
+- All the other things provided by ClusterProvider, which is:
+-- ClusterStartupTime (used for offlineSessions and revokeRefreshToken) is shared for all clusters in all datacenters
+-- Periodic userStorage synchronization is always executed just on one node at a time. It won't be never executed concurrently on more nodes (Assuming "nodes" refer to all servers in all clusters in all datacenters)
+
+What doesn't work right now:
+- UserSessionProvider and offline sessions
+  
+
+Basic setup
+===========
+
+This is setup with 2 keycloak nodes, which are NOT in cluster. They just share the same database and they will be configured with "work" infinispan cache with remoteStore, which will point
+to external JDG server.
+ 
+JDG Server setup
+----------------
+- Download JDG 7.0 server and unzip to some folder
+
+- Add this into JDG_HOME/standalone/configuration/standalone.xml under cache-container named "local" :
+
+```
+<local-cache name="work" start="EAGER" batching="false" />
+```
+
+- Start server:
+```
+cd JDG_HOME/bin
+./standalone.sh -Djboss.socket.binding.port-offset=100
+```
+
+Keycloak servers setup
+----------------------
+You need to setup 2 Keycloak nodes in this way. 
+
+For now, it's recommended to test Keycloak overlay on EAP7 because of infinispan bug, which is fixed in EAP 7.0 (infinispan 8.1.2), but not 
+yet on Wildfly 10 (infinispan 8.1.0). See below for details.
+
+1) Configure shared database in KEYCLOAK_HOME/standalone/configuration/standalone.xml . For example MySQL
+
+2) Add `module` attribute to the infinispan keycloak container:
+  
+```  
+<cache-container name="keycloak" jndi-name="infinispan/Keycloak" module="org.keycloak.keycloak-model-infinispan">
+```
+  
+3) Configure `work` cache to use remoteStore. You should use this:  
+
+```
+<local-cache name="work">
+    <remote-store passivation="false" fetch-state="false" purge="false" preload="false" shared="true" cache="work" remote-servers="remote-cache">    
+        <property name="rawValues">true</property>
+        <property name="marshaller">org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory</property>
+    </remote-store>
+</local-cache>  
+```
+
+4) Configure connection to the external JDG server. Because we used port offset 100 for JDG (see above), the HotRod endpoint is running on 11322 . 
+So add the config like this to the bottom of standalone.xml under `socket-binding-group` element:
+
+```
+<outbound-socket-binding name="remote-cache">
+    <remote-destination host="localhost" port="11322"/>
+</outbound-socket-binding>
+```
+
+5) Optional: Configure logging in standalone.xml to see what invalidation events were send:
+````
+<logger category="org.keycloak.cluster.infinispan">
+    <level name="TRACE"/>
+</logger>
+<logger category="org.keycloak.models.cache.infinispan">
+    <level name="DEBUG"/>
+</logger>
+````
+           
+6)  Setup Keycloak node2 . Just copy Keycloak to another location on your laptop and repeat steps 1-5 above for second server too.
+          
+7) Run server 1 with parameters like (assuming you have virtual hosts "node1" and "node2" defined in your `/etc/hosts` ):
+```           
+./standalone.sh -Djboss.node.name=node1 -b node1 -bmanagement node1
+```
+
+and server2 with:
+```
+./standalone.sh -Djboss.node.name=node2 -b node2 -bmanagement node2
+```
+
+8) Note something like this in both `KEYCLOAK_HOME/standalone/log/server.log` on both nodes. Note that cluster Startup Time will be same time on both nodes:
+```
+2016-11-16 22:12:52,080 DEBUG [org.keycloak.cluster.infinispan.InfinispanClusterProviderFactory] (ServerService Thread Pool -- 62) My address: node1-1953169551
+2016-11-16 22:12:52,081 DEBUG [org.keycloak.cluster.infinispan.CrossDCAwareCacheFactory] (ServerService Thread Pool -- 62) RemoteStore is available. Cross-DC scenario will be used
+2016-11-16 22:12:52,119 DEBUG [org.keycloak.cluster.infinispan.InfinispanClusterProviderFactory] (ServerService Thread Pool -- 62) Loaded cluster startup time: Wed Nov 16 22:09:48 CET 2016
+2016-11-16 22:12:52,128 DEBUG [org.keycloak.cluster.infinispan.InfinispanNotificationsManager] (ServerService Thread Pool -- 62) Added listener for HotRod remoteStore cache: work
+```
+
+9) Login to node1. Then change any realm on node2. You will see in the node2 server.log that RealmUpdatedEvent was sent and on node1 that this event was received. 
+
+This is done even if node1 and node2 are NOT in cluster as it's the external JDG used for communication between 2 keycloak servers and sending/receiving cache invalidation events. But note that userSession
+doesn't yet work (eg. if you login to node1, you won't see the userSession on node2).
+
+
+WARNING: Previous steps works on Keycloak server overlay deployed on EAP 7.0 . With deploy on Wildfly 10.0.0.Final, you will see exception 
+at startup caused by the bug https://issues.jboss.org/browse/ISPN-6203 .
+
+There is a workaround to add this line into KEYCLOAK_HOME/modules/system/layers/base/org/wildfly/clustering/service/main/module.xml :
+
+```
+<module name="org.infinispan.client.hotrod"/>
+```
diff --git a/model/infinispan/pom.xml b/model/infinispan/pom.xml
index f10fa60..fba921c 100755
--- a/model/infinispan/pom.xml
+++ b/model/infinispan/pom.xml
@@ -49,6 +49,10 @@
             <artifactId>infinispan-core</artifactId>
         </dependency>
         <dependency>
+            <groupId>org.infinispan</groupId>
+            <artifactId>infinispan-cachestore-remote</artifactId>
+        </dependency>
+        <dependency>
             <groupId>junit</groupId>
             <artifactId>junit</artifactId>
             <scope>test</scope>
diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/CrossDCAwareCacheFactory.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/CrossDCAwareCacheFactory.java
new file mode 100644
index 0000000..17795ca
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/CrossDCAwareCacheFactory.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.cluster.infinispan;
+
+import java.io.Serializable;
+import java.util.Set;
+
+import org.infinispan.Cache;
+import org.infinispan.client.hotrod.Flag;
+import org.infinispan.client.hotrod.RemoteCache;
+import org.infinispan.commons.api.BasicCache;
+import org.infinispan.persistence.remote.RemoteStore;
+import org.jboss.logging.Logger;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+abstract class CrossDCAwareCacheFactory {
+
+    protected static final Logger logger = Logger.getLogger(CrossDCAwareCacheFactory.class);
+
+
+    abstract BasicCache<String, Serializable> getCache();
+
+
+    static CrossDCAwareCacheFactory getFactory(Cache<String, Serializable> workCache, Set<RemoteStore> remoteStores) {
+        if (remoteStores.isEmpty()) {
+            logger.debugf("No configured remoteStore available. Cross-DC scenario is not used");
+            return new InfinispanCacheWrapperFactory(workCache);
+        } else {
+            logger.debugf("RemoteStore is available. Cross-DC scenario will be used");
+
+            if (remoteStores.size() > 1) {
+                logger.warnf("More remoteStores configured for work cache. Will use just the first one");
+            }
+
+            // For cross-DC scenario, we need to return underlying remoteCache for atomic operations to work properly
+            RemoteStore remoteStore = remoteStores.iterator().next();
+            RemoteCache remoteCache = remoteStore.getRemoteCache();
+            return new RemoteCacheWrapperFactory(remoteCache);
+        }
+    }
+
+
+    // We don't have external JDG configured. No cross-DC.
+    private static class InfinispanCacheWrapperFactory extends CrossDCAwareCacheFactory {
+
+        private final Cache<String, Serializable> workCache;
+
+        InfinispanCacheWrapperFactory(Cache<String, Serializable> workCache) {
+            this.workCache = workCache;
+        }
+
+        @Override
+        BasicCache<String, Serializable> getCache() {
+            return workCache;
+        }
+
+    }
+
+
+    // We have external JDG configured. Cross-DC should be enabled
+    private static class RemoteCacheWrapperFactory extends CrossDCAwareCacheFactory {
+
+        private final RemoteCache<String, Serializable> remoteCache;
+
+        RemoteCacheWrapperFactory(RemoteCache<String, Serializable> remoteCache) {
+            this.remoteCache = remoteCache;
+        }
+
+        @Override
+        BasicCache<String, Serializable> getCache() {
+            // Flags are per-invocation!
+            return remoteCache.withFlags(Flag.FORCE_RETURN_VALUE);
+        }
+
+    }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProvider.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProvider.java
index 8b77c25..5a4bdb7 100644
--- a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProvider.java
+++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProvider.java
@@ -17,20 +17,15 @@
 
 package org.keycloak.cluster.infinispan;
 
-import org.infinispan.Cache;
-import org.infinispan.context.Flag;
-import org.infinispan.lifecycle.ComponentStatus;
-import org.infinispan.remoting.transport.Transport;
 import org.jboss.logging.Logger;
 import org.keycloak.cluster.ClusterEvent;
 import org.keycloak.cluster.ClusterListener;
 import org.keycloak.cluster.ClusterProvider;
 import org.keycloak.cluster.ExecutionResult;
 import org.keycloak.common.util.Time;
-import org.keycloak.models.KeycloakSession;
 
-import java.io.Serializable;
 import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
 
 /**
  *
@@ -43,34 +38,22 @@ public class InfinispanClusterProvider implements ClusterProvider {
     public static final String CLUSTER_STARTUP_TIME_KEY = "cluster-start-time";
     private static final String TASK_KEY_PREFIX = "task::";
 
-    private final InfinispanClusterProviderFactory factory;
-    private final KeycloakSession session;
-    private final Cache<String, Serializable> cache;
+    private final int clusterStartupTime;
+    private final String myAddress;
+    private final CrossDCAwareCacheFactory crossDCAwareCacheFactory;
+    private final InfinispanNotificationsManager notificationsManager; // Just to extract notifications related stuff to separate class
 
-    public InfinispanClusterProvider(InfinispanClusterProviderFactory factory, KeycloakSession session, Cache<String, Serializable> cache) {
-        this.factory = factory;
-        this.session = session;
-        this.cache = cache;
+    public InfinispanClusterProvider(int clusterStartupTime, String myAddress, CrossDCAwareCacheFactory crossDCAwareCacheFactory, InfinispanNotificationsManager notificationsManager) {
+        this.myAddress = myAddress;
+        this.clusterStartupTime = clusterStartupTime;
+        this.crossDCAwareCacheFactory = crossDCAwareCacheFactory;
+        this.notificationsManager = notificationsManager;
     }
 
 
     @Override
     public int getClusterStartupTime() {
-        Integer existingClusterStartTime = (Integer) cache.get(InfinispanClusterProvider.CLUSTER_STARTUP_TIME_KEY);
-        if (existingClusterStartTime != null) {
-            return existingClusterStartTime;
-        } else {
-            // clusterStartTime not yet initialized. Let's try to put our startupTime
-            int serverStartTime = (int) (session.getKeycloakSessionFactory().getServerStartupTimestamp() / 1000);
-
-            existingClusterStartTime = (Integer) cache.putIfAbsent(InfinispanClusterProvider.CLUSTER_STARTUP_TIME_KEY, serverStartTime);
-            if (existingClusterStartTime == null) {
-                logger.debugf("Initialized cluster startup time to %s", Time.toDate(serverStartTime).toString());
-                return serverStartTime;
-            } else {
-                return existingClusterStartTime;
-            }
-        }
+        return clusterStartupTime;
     }
 
 
@@ -104,56 +87,33 @@ public class InfinispanClusterProvider implements ClusterProvider {
 
     @Override
     public void registerListener(String taskKey, ClusterListener task) {
-        factory.registerListener(taskKey, task);
+        this.notificationsManager.registerListener(taskKey, task);
     }
 
 
     @Override
-    public void notify(String taskKey, ClusterEvent event) {
-        // Put the value to the cache to notify listeners on all the nodes
-        cache.put(taskKey, event);
+    public void notify(String taskKey, ClusterEvent event, boolean ignoreSender) {
+        this.notificationsManager.notify(taskKey, event, ignoreSender);
     }
 
 
-    private String getCurrentNode(Cache<String, Serializable> cache) {
-        Transport transport = cache.getCacheManager().getTransport();
-        return transport==null ? "local" : transport.getAddress().toString();
-    }
-
-
-    private LockEntry createLockEntry(Cache<String, Serializable> cache) {
+    private LockEntry createLockEntry() {
         LockEntry lock = new LockEntry();
-        lock.setNode(getCurrentNode(cache));
+        lock.setNode(myAddress);
         lock.setTimestamp(Time.currentTime());
         return lock;
     }
 
 
     private boolean tryLock(String cacheKey, int taskTimeoutInSeconds) {
-        LockEntry myLock = createLockEntry(cache);
+        LockEntry myLock = createLockEntry();
 
-        LockEntry existingLock = (LockEntry) cache.putIfAbsent(cacheKey, myLock);
+        LockEntry existingLock = (LockEntry) crossDCAwareCacheFactory.getCache().putIfAbsent(cacheKey, myLock, taskTimeoutInSeconds, TimeUnit.SECONDS);
         if (existingLock != null) {
-            // Task likely already in progress. Check if timestamp is not outdated
-            int thatTime = existingLock.getTimestamp();
-            int currentTime = Time.currentTime();
-            if (thatTime + taskTimeoutInSeconds < currentTime) {
-                if (logger.isTraceEnabled()) {
-                    logger.tracef("Task %s outdated when in progress by node %s. Will try to replace task with our node %s", cacheKey, existingLock.getNode(), myLock.getNode());
-                }
-                boolean replaced = cache.replace(cacheKey, existingLock, myLock);
-                if (!replaced) {
-                    if (logger.isTraceEnabled()) {
-                        logger.tracef("Failed to replace the task %s. Other thread replaced in the meantime. Ignoring task.", cacheKey);
-                    }
-                }
-                return replaced;
-            } else {
-                if (logger.isTraceEnabled()) {
-                    logger.tracef("Task %s in progress already by node %s. Ignoring task.", cacheKey, existingLock.getNode());
-                }
-                return false;
+            if (logger.isTraceEnabled()) {
+                logger.tracef("Task %s in progress already by node %s. Ignoring task.", cacheKey, existingLock.getNode());
             }
+            return false;
         } else {
             if (logger.isTraceEnabled()) {
                 logger.tracef("Successfully acquired lock for task %s. Our node is %s", cacheKey, myLock.getNode());
@@ -168,20 +128,12 @@ public class InfinispanClusterProvider implements ClusterProvider {
         int retry = 3;
         while (true) {
             try {
-                cache.getAdvancedCache()
-                        .withFlags(Flag.IGNORE_RETURN_VALUES, Flag.FORCE_SYNCHRONOUS)
-                        .remove(cacheKey);
+                crossDCAwareCacheFactory.getCache().remove(cacheKey);
                 if (logger.isTraceEnabled()) {
                     logger.tracef("Task %s removed from the cache", cacheKey);
                 }
                 return;
             } catch (RuntimeException e) {
-                ComponentStatus status = cache.getStatus();
-                if (status.isStopping() || status.isTerminated()) {
-                    logger.warnf("Failed to remove task %s from the cache. Cache is already terminating", cacheKey);
-                    logger.debug(e.getMessage(), e);
-                    return;
-                }
                 retry--;
                 if (retry == 0) {
                     throw e;
diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProviderFactory.java
index 75aef45..a96621d 100644
--- a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProviderFactory.java
@@ -20,27 +20,24 @@ package org.keycloak.cluster.infinispan;
 import org.infinispan.Cache;
 import org.infinispan.manager.EmbeddedCacheManager;
 import org.infinispan.notifications.Listener;
-import org.infinispan.notifications.cachelistener.annotation.CacheEntryCreated;
-import org.infinispan.notifications.cachelistener.annotation.CacheEntryModified;
-import org.infinispan.notifications.cachelistener.event.CacheEntryCreatedEvent;
-import org.infinispan.notifications.cachelistener.event.CacheEntryModifiedEvent;
 import org.infinispan.notifications.cachemanagerlistener.annotation.ViewChanged;
 import org.infinispan.notifications.cachemanagerlistener.event.ViewChangedEvent;
+import org.infinispan.persistence.manager.PersistenceManager;
+import org.infinispan.persistence.remote.RemoteStore;
 import org.infinispan.remoting.transport.Address;
 import org.infinispan.remoting.transport.Transport;
 import org.jboss.logging.Logger;
 import org.keycloak.Config;
-import org.keycloak.cluster.ClusterEvent;
-import org.keycloak.cluster.ClusterListener;
 import org.keycloak.cluster.ClusterProvider;
 import org.keycloak.cluster.ClusterProviderFactory;
+import org.keycloak.common.util.HostUtils;
+import org.keycloak.common.util.Time;
 import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
 
 import java.io.Serializable;
 import java.util.Collection;
-import java.util.HashMap;
 import java.util.Iterator;
 import java.util.Map;
 import java.util.Set;
@@ -49,6 +46,8 @@ import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
 /**
+ * This impl is aware of Cross-Data-Center scenario too
+ *
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
  */
 public class InfinispanClusterProviderFactory implements ClusterProviderFactory {
@@ -57,28 +56,82 @@ public class InfinispanClusterProviderFactory implements ClusterProviderFactory 
 
     protected static final Logger logger = Logger.getLogger(InfinispanClusterProviderFactory.class);
 
+    // Infinispan cache
     private volatile Cache<String, Serializable> workCache;
 
-    private Map<String, ClusterListener> listeners = new HashMap<>();
+    // Ensure that atomic operations (like putIfAbsent) must work correctly in any of: non-clustered, clustered or cross-Data-Center (cross-DC) setups
+    private CrossDCAwareCacheFactory crossDCAwareCacheFactory;
+
+    private String myAddress;
+
+    private int clusterStartupTime;
+
+    // Just to extract notifications related stuff to separate class
+    private InfinispanNotificationsManager notificationsManager;
 
     @Override
     public ClusterProvider create(KeycloakSession session) {
         lazyInit(session);
-        return new InfinispanClusterProvider(this, session, workCache);
+        return new InfinispanClusterProvider(clusterStartupTime, myAddress, crossDCAwareCacheFactory, notificationsManager);
     }
 
     private void lazyInit(KeycloakSession session) {
         if (workCache == null) {
             synchronized (this) {
                 if (workCache == null) {
-                    workCache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.WORK_CACHE_NAME);
+                    InfinispanConnectionProvider ispnConnections = session.getProvider(InfinispanConnectionProvider.class);
+                    workCache = ispnConnections.getCache(InfinispanConnectionProvider.WORK_CACHE_NAME);
+
                     workCache.getCacheManager().addListener(new ViewChangeListener());
-                    workCache.addListener(new CacheEntryListener());
+                    initMyAddress();
+
+                    Set<RemoteStore> remoteStores = getRemoteStores();
+                    crossDCAwareCacheFactory = CrossDCAwareCacheFactory.getFactory(workCache, remoteStores);
+
+                    clusterStartupTime = initClusterStartupTime(session);
+
+                    notificationsManager = InfinispanNotificationsManager.create(workCache, myAddress, remoteStores);
                 }
             }
         }
     }
 
+
+    // See if we have RemoteStore (external JDG) configured for cross-Data-Center scenario
+    private Set<RemoteStore> getRemoteStores() {
+        return workCache.getAdvancedCache().getComponentRegistry().getComponent(PersistenceManager.class).getStores(RemoteStore.class);
+    }
+
+
+    protected void initMyAddress() {
+        Transport transport = workCache.getCacheManager().getTransport();
+        this.myAddress = transport == null ? HostUtils.getHostName() + "-" + workCache.hashCode() : transport.getAddress().toString();
+        logger.debugf("My address: %s", this.myAddress);
+    }
+
+
+    protected int initClusterStartupTime(KeycloakSession session) {
+        Integer existingClusterStartTime = (Integer) crossDCAwareCacheFactory.getCache().get(InfinispanClusterProvider.CLUSTER_STARTUP_TIME_KEY);
+        if (existingClusterStartTime != null) {
+            logger.debugf("Loaded cluster startup time: %s", Time.toDate(existingClusterStartTime).toString());
+            return existingClusterStartTime;
+        } else {
+            // clusterStartTime not yet initialized. Let's try to put our startupTime
+            int serverStartTime = (int) (session.getKeycloakSessionFactory().getServerStartupTimestamp() / 1000);
+
+            existingClusterStartTime = (Integer) crossDCAwareCacheFactory.getCache().putIfAbsent(InfinispanClusterProvider.CLUSTER_STARTUP_TIME_KEY, serverStartTime);
+            if (existingClusterStartTime == null) {
+                logger.debugf("Initialized cluster startup time to %s", Time.toDate(serverStartTime).toString());
+                return serverStartTime;
+            } else {
+                logger.debugf("Loaded cluster startup time: %s", Time.toDate(existingClusterStartTime).toString());
+                return existingClusterStartTime;
+            }
+        }
+    }
+
+
+
     @Override
     public void init(Config.Scope config) {
     }
@@ -167,34 +220,4 @@ public class InfinispanClusterProviderFactory implements ClusterProviderFactory 
     }
 
 
-    <T> void registerListener(String taskKey, ClusterListener task) {
-        listeners.put(taskKey, task);
-    }
-
-    @Listener
-    public class CacheEntryListener {
-
-        @CacheEntryCreated
-        public void cacheEntryCreated(CacheEntryCreatedEvent<String, Object> event) {
-            if (!event.isPre()) {
-                trigger(event.getKey(), event.getValue());
-            }
-        }
-
-        @CacheEntryModified
-        public void cacheEntryModified(CacheEntryModifiedEvent<String, Object> event) {
-            if (!event.isPre()) {
-                trigger(event.getKey(), event.getValue());
-            }
-        }
-
-        private void trigger(String key, Object value) {
-            ClusterListener task = listeners.get(key);
-            if (task != null) {
-                ClusterEvent event = (ClusterEvent) value;
-                task.run(event);
-            }
-        }
-    }
-
 }
diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanNotificationsManager.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanNotificationsManager.java
new file mode 100644
index 0000000..57cc003
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanNotificationsManager.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.cluster.infinispan;
+
+import java.io.Serializable;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import org.infinispan.Cache;
+import org.infinispan.client.hotrod.RemoteCache;
+import org.infinispan.client.hotrod.annotation.ClientCacheEntryCreated;
+import org.infinispan.client.hotrod.annotation.ClientCacheEntryModified;
+import org.infinispan.client.hotrod.annotation.ClientListener;
+import org.infinispan.client.hotrod.event.ClientCacheEntryCreatedEvent;
+import org.infinispan.client.hotrod.event.ClientCacheEntryModifiedEvent;
+import org.infinispan.client.hotrod.event.ClientEvent;
+import org.infinispan.context.Flag;
+import org.infinispan.marshall.core.MarshalledEntry;
+import org.infinispan.notifications.Listener;
+import org.infinispan.notifications.cachelistener.annotation.CacheEntryCreated;
+import org.infinispan.notifications.cachelistener.annotation.CacheEntryModified;
+import org.infinispan.notifications.cachelistener.event.CacheEntryCreatedEvent;
+import org.infinispan.notifications.cachelistener.event.CacheEntryModifiedEvent;
+import org.infinispan.persistence.manager.PersistenceManager;
+import org.infinispan.persistence.remote.RemoteStore;
+import org.infinispan.remoting.transport.Transport;
+import org.jboss.logging.Logger;
+import org.keycloak.cluster.ClusterEvent;
+import org.keycloak.cluster.ClusterListener;
+import org.keycloak.cluster.ClusterProvider;
+import org.keycloak.common.util.HostUtils;
+import org.keycloak.common.util.MultivaluedHashMap;
+
+/**
+ * Impl for sending infinispan messages across cluster and listening to them
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class InfinispanNotificationsManager {
+
+    protected static final Logger logger = Logger.getLogger(InfinispanNotificationsManager.class);
+
+    private final MultivaluedHashMap<String, ClusterListener> listeners = new MultivaluedHashMap<>();
+
+    private final Cache<String, Serializable> workCache;
+
+    private final String myAddress;
+
+
+    protected InfinispanNotificationsManager(Cache<String, Serializable> workCache, String myAddress) {
+        this.workCache = workCache;
+        this.myAddress = myAddress;
+    }
+
+
+    // Create and init manager including all listeners etc
+    public static InfinispanNotificationsManager create(Cache<String, Serializable> workCache, String myAddress, Set<RemoteStore> remoteStores) {
+        InfinispanNotificationsManager manager = new InfinispanNotificationsManager(workCache, myAddress);
+
+        // We need CacheEntryListener just if we don't have remoteStore. With remoteStore will be all cluster nodes notified anyway from HotRod listener
+        if (remoteStores.isEmpty()) {
+            workCache.addListener(manager.new CacheEntryListener());
+
+            logger.debugf("Added listener for infinispan cache: %s", workCache.getName());
+        } else {
+            for (RemoteStore remoteStore : remoteStores) {
+                RemoteCache<Object, Object> remoteCache = remoteStore.getRemoteCache();
+                remoteCache.addClientListener(manager.new HotRodListener(remoteCache));
+
+                logger.debugf("Added listener for HotRod remoteStore cache: %s", remoteCache.getName());
+            }
+        }
+
+        return manager;
+    }
+
+
+    void registerListener(String taskKey, ClusterListener task) {
+        listeners.add(taskKey, task);
+    }
+
+
+    void notify(String taskKey, ClusterEvent event, boolean ignoreSender) {
+        WrapperClusterEvent wrappedEvent = new WrapperClusterEvent();
+        wrappedEvent.setDelegateEvent(event);
+        wrappedEvent.setIgnoreSender(ignoreSender);
+        wrappedEvent.setSender(myAddress);
+
+        if (logger.isTraceEnabled()) {
+            logger.tracef("Sending event %s: %s", taskKey, event);
+        }
+
+        // Put the value to the cache to notify listeners on all the nodes
+        workCache.getAdvancedCache().withFlags(Flag.IGNORE_RETURN_VALUES)
+                .put(taskKey, wrappedEvent, 120, TimeUnit.SECONDS);
+    }
+
+
+    @Listener(observation = Listener.Observation.POST)
+    public class CacheEntryListener {
+
+        @CacheEntryCreated
+        public void cacheEntryCreated(CacheEntryCreatedEvent<String, Serializable> event) {
+            eventReceived(event.getKey(), event.getValue());
+        }
+
+        @CacheEntryModified
+        public void cacheEntryModified(CacheEntryModifiedEvent<String, Serializable> event) {
+            eventReceived(event.getKey(), event.getValue());
+        }
+    }
+
+
+    @ClientListener
+    public class HotRodListener {
+
+        private final RemoteCache<Object, Object> remoteCache;
+
+        public HotRodListener(RemoteCache<Object, Object> remoteCache) {
+            this.remoteCache = remoteCache;
+        }
+
+
+        @ClientCacheEntryCreated
+        public void created(ClientCacheEntryCreatedEvent event) {
+            String key = event.getKey().toString();
+            hotrodEventReceived(key);
+        }
+
+
+        @ClientCacheEntryModified
+        public void updated(ClientCacheEntryModifiedEvent event) {
+            String key = event.getKey().toString();
+            hotrodEventReceived(key);
+        }
+
+        private void hotrodEventReceived(String key) {
+            // TODO: Look at CacheEventConverter stuff to possibly include value in the event and avoid additional remoteCache request
+            Object value = remoteCache.get(key);
+
+            Serializable rawValue;
+            if (value instanceof MarshalledEntry) {
+                Object rw = ((MarshalledEntry)value).getValue();
+                rawValue = (Serializable) rw;
+            } else {
+                rawValue = (Serializable) value;
+            }
+
+
+            eventReceived(key, rawValue);
+        }
+
+    }
+
+    private void eventReceived(String key, Serializable obj) {
+        if (!(obj instanceof WrapperClusterEvent)) {
+            return;
+        }
+
+        WrapperClusterEvent event = (WrapperClusterEvent) obj;
+
+        if (event.isIgnoreSender()) {
+            if (this.myAddress.equals(event.getSender())) {
+                return;
+            }
+        }
+
+        if (logger.isTraceEnabled()) {
+            logger.tracef("Received event %s: %s", key, event);
+        }
+
+        ClusterEvent wrappedEvent = event.getDelegateEvent();
+
+        List<ClusterListener> myListeners = listeners.get(key);
+        if (myListeners != null) {
+            for (ClusterListener listener : myListeners) {
+                listener.eventReceived(wrappedEvent);
+            }
+        }
+
+        myListeners = listeners.get(ClusterProvider.ALL);
+        if (myListeners != null) {
+            for (ClusterListener listener : myListeners) {
+                listener.eventReceived(wrappedEvent);
+            }
+        }
+    }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/KeycloakHotRodMarshallerFactory.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/KeycloakHotRodMarshallerFactory.java
new file mode 100644
index 0000000..4a73bf3
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/KeycloakHotRodMarshallerFactory.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.cluster.infinispan;
+
+import org.infinispan.commons.marshall.jboss.GenericJBossMarshaller;
+
+/**
+ * Needed on Wildfly, so that remoteStore (hotRod client) can find our classes
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class KeycloakHotRodMarshallerFactory {
+
+    public static GenericJBossMarshaller getInstance() {
+        return new GenericJBossMarshaller(KeycloakHotRodMarshallerFactory.class.getClassLoader());
+    }
+
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/WrapperClusterEvent.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/WrapperClusterEvent.java
new file mode 100644
index 0000000..b03dd70
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/WrapperClusterEvent.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.cluster.infinispan;
+
+import org.keycloak.cluster.ClusterEvent;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class WrapperClusterEvent implements ClusterEvent {
+
+    private String sender; // will be null in non-clustered environment
+    private boolean ignoreSender;
+    private ClusterEvent delegateEvent;
+
+    public String getSender() {
+        return sender;
+    }
+
+    public void setSender(String sender) {
+        this.sender = sender;
+    }
+
+    public boolean isIgnoreSender() {
+        return ignoreSender;
+    }
+
+    public void setIgnoreSender(boolean ignoreSender) {
+        this.ignoreSender = ignoreSender;
+    }
+
+    public ClusterEvent getDelegateEvent() {
+        return delegateEvent;
+    }
+
+    public void setDelegateEvent(ClusterEvent delegateEvent) {
+        this.delegateEvent = delegateEvent;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("WrapperClusterEvent [ sender=%s, delegateEvent=%s ]", sender, delegateEvent.toString());
+    }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java
index 8ad75fd..7781e3a 100755
--- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java
@@ -27,11 +27,14 @@ import org.infinispan.eviction.EvictionStrategy;
 import org.infinispan.eviction.EvictionType;
 import org.infinispan.manager.DefaultCacheManager;
 import org.infinispan.manager.EmbeddedCacheManager;
+import org.infinispan.persistence.remote.configuration.ExhaustedAction;
+import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder;
 import org.infinispan.transaction.LockingMode;
 import org.infinispan.transaction.TransactionMode;
 import org.infinispan.transaction.lookup.DummyTransactionManagerLookup;
 import org.jboss.logging.Logger;
 import org.keycloak.Config;
+import org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
 
@@ -126,7 +129,7 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
         GlobalConfigurationBuilder gcb = new GlobalConfigurationBuilder();
 
         boolean clustered = config.getBoolean("clustered", false);
-        boolean async = config.getBoolean("async", true);
+        boolean async = config.getBoolean("async", false);
         boolean allowDuplicateJMXDomains = config.getBoolean("allowDuplicateJMXDomains", true);
 
         if (clustered) {
@@ -139,14 +142,11 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
 
         logger.debug("Started embedded Infinispan cache container");
 
-        ConfigurationBuilder invalidationConfigBuilder = new ConfigurationBuilder();
-        if (clustered) {
-            invalidationConfigBuilder.clustering().cacheMode(async ? CacheMode.INVALIDATION_ASYNC : CacheMode.INVALIDATION_SYNC);
-        }
-        Configuration invalidationCacheConfiguration = invalidationConfigBuilder.build();
+        ConfigurationBuilder modelCacheConfigBuilder = new ConfigurationBuilder();
+        Configuration modelCacheConfiguration = modelCacheConfigBuilder.build();
 
-        cacheManager.defineConfiguration(InfinispanConnectionProvider.REALM_CACHE_NAME, invalidationCacheConfiguration);
-        cacheManager.defineConfiguration(InfinispanConnectionProvider.USER_CACHE_NAME, invalidationCacheConfiguration);
+        cacheManager.defineConfiguration(InfinispanConnectionProvider.REALM_CACHE_NAME, modelCacheConfiguration);
+        cacheManager.defineConfiguration(InfinispanConnectionProvider.USER_CACHE_NAME, modelCacheConfiguration);
 
         ConfigurationBuilder sessionConfigBuilder = new ConfigurationBuilder();
         if (clustered) {
@@ -174,8 +174,14 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
         if (clustered) {
             replicationConfigBuilder.clustering().cacheMode(async ? CacheMode.REPL_ASYNC : CacheMode.REPL_SYNC);
         }
-        Configuration replicationCacheConfiguration = replicationConfigBuilder.build();
-        cacheManager.defineConfiguration(InfinispanConnectionProvider.WORK_CACHE_NAME, replicationCacheConfiguration);
+
+        boolean jdgEnabled = config.getBoolean("remoteStoreEnabled", false);
+        if (jdgEnabled) {
+            configureRemoteCacheStore(replicationConfigBuilder, async);
+        }
+
+        Configuration replicationEvictionCacheConfiguration = replicationConfigBuilder.build();
+        cacheManager.defineConfiguration(InfinispanConnectionProvider.WORK_CACHE_NAME, replicationEvictionCacheConfiguration);
 
         ConfigurationBuilder counterConfigBuilder = new ConfigurationBuilder();
         counterConfigBuilder.invocationBatching().enable()
@@ -211,6 +217,34 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
         return cb.build();
     }
 
+    // Used for cross-data centers scenario. Usually integration with external JDG server, which itself handles communication between DCs.
+    private void configureRemoteCacheStore(ConfigurationBuilder builder, boolean async) {
+        String jdgServer = config.get("remoteStoreServer", "localhost");
+        Integer jdgPort = config.getInt("remoteStorePort", 11222);
+
+        builder.persistence()
+                .passivation(false)
+                .addStore(RemoteStoreConfigurationBuilder.class)
+                    .fetchPersistentState(false)
+                    .ignoreModifications(false)
+                    .purgeOnStartup(false)
+                    .preload(false)
+                    .shared(true)
+                    .remoteCacheName(InfinispanConnectionProvider.WORK_CACHE_NAME)
+                    .rawValues(true)
+                    .forceReturnValues(false)
+                    .marshaller(KeycloakHotRodMarshallerFactory.class.getName())
+                    .addServer()
+                        .host(jdgServer)
+                        .port(jdgPort)
+//                  .connectionPool()
+//                      .maxActive(100)
+//                      .exhaustedAction(ExhaustedAction.CREATE_NEW)
+                    .async()
+                        .enabled(async);
+
+    }
+
     protected Configuration getKeysCacheConfig() {
         ConfigurationBuilder cb = new ConfigurationBuilder();
         cb.eviction().strategy(EvictionStrategy.LRU).type(EvictionType.COUNT).size(InfinispanConnectionProvider.KEYS_CACHE_DEFAULT_MAX);
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/CacheManager.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/CacheManager.java
index ad1ba26..c254ea7 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/CacheManager.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/CacheManager.java
@@ -1,14 +1,13 @@
 package org.keycloak.models.cache.infinispan;
 
 import org.infinispan.Cache;
-import org.infinispan.notifications.cachelistener.annotation.CacheEntriesEvicted;
-import org.infinispan.notifications.cachelistener.annotation.CacheEntryInvalidated;
-import org.infinispan.notifications.cachelistener.event.CacheEntriesEvictedEvent;
-import org.infinispan.notifications.cachelistener.event.CacheEntryInvalidatedEvent;
 import org.jboss.logging.Logger;
-import org.keycloak.models.cache.infinispan.entities.AbstractRevisioned;
+import org.keycloak.cluster.ClusterProvider;
+import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
+import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.cache.infinispan.entities.Revisioned;
 
+import java.util.Collection;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.Map;
@@ -55,7 +54,7 @@ import java.util.function.Predicate;
  * @version $Revision: 1 $
  */
 public abstract class CacheManager {
-    protected static final Logger logger = Logger.getLogger(CacheManager.class);
+
     protected final Cache<String, Long> revisions;
     protected final Cache<String, Revisioned> cache;
     protected final UpdateCounter counter = new UpdateCounter();
@@ -63,9 +62,10 @@ public abstract class CacheManager {
     public CacheManager(Cache<String, Revisioned> cache, Cache<String, Long> revisions) {
         this.cache = cache;
         this.revisions = revisions;
-        this.cache.addListener(this);
     }
 
+    protected abstract Logger getLogger();
+
     public Cache<String, Revisioned> getCache() {
         return cache;
     }
@@ -79,10 +79,7 @@ public abstract class CacheManager {
         if (revision == null) {
             revision = counter.current();
         }
-        // if you do cache.remove() on node 1 and the entry doesn't exist on node 2, node 2 never receives a invalidation event
-        // so, we do this to force this.
-        String invalidationKey = "invalidation.key" + id;
-        cache.putForExternalRead(invalidationKey, new AbstractRevisioned(-1L, invalidationKey));
+
         return revision;
     }
 
@@ -101,12 +98,16 @@ public abstract class CacheManager {
         }
         Long rev = revisions.get(id);
         if (rev == null) {
-            RealmCacheManager.logger.tracev("get() missing rev");
+            if (getLogger().isTraceEnabled()) {
+                getLogger().tracev("get() missing rev {0}", id);
+            }
             return null;
         }
         long oRev = o.getRevision() == null ? -1L : o.getRevision().longValue();
         if (rev > oRev) {
-            RealmCacheManager.logger.tracev("get() rev: {0} o.rev: {1}", rev.longValue(), oRev);
+            if (getLogger().isTraceEnabled()) {
+                getLogger().tracev("get() rev: {0} o.rev: {1}", rev.longValue(), oRev);
+            }
             return null;
         }
         return o != null && type.isInstance(o) ? type.cast(o) : null;
@@ -114,9 +115,11 @@ public abstract class CacheManager {
 
     public Object invalidateObject(String id) {
         Revisioned removed = (Revisioned)cache.remove(id);
-        // if you do cache.remove() on node 1 and the entry doesn't exist on node 2, node 2 never receives a invalidation event
-        // so, we do this to force the event.
-        cache.remove("invalidation.key" + id);
+
+        if (getLogger().isTraceEnabled()) {
+            getLogger().tracef("Removed key='%s', value='%s' from cache", id, removed);
+        }
+
         bumpVersion(id);
         return removed;
     }
@@ -137,37 +140,35 @@ public abstract class CacheManager {
             //revisions.getAdvancedCache().lock(id);
             Long rev = revisions.get(id);
             if (rev == null) {
-                if (id.endsWith("realm.clients")) RealmCacheManager.logger.trace("addRevisioned rev == null realm.clients");
                 rev = counter.current();
                 revisions.put(id, rev);
             }
             revisions.startBatch();
             if (!revisions.getAdvancedCache().lock(id)) {
-                RealmCacheManager.logger.trace("Could not obtain version lock");
+                if (getLogger().isTraceEnabled()) {
+                    getLogger().tracev("Could not obtain version lock: {0}", id);
+                }
                 return;
             }
             rev = revisions.get(id);
             if (rev == null) {
-                if (id.endsWith("realm.clients")) RealmCacheManager.logger.trace("addRevisioned rev2 == null realm.clients");
                 return;
             }
             if (rev > startupRevision) { // revision is ahead transaction start. Other transaction updated in the meantime. Don't cache
-                if (RealmCacheManager.logger.isTraceEnabled()) {
-                    RealmCacheManager.logger.tracev("Skipped cache. Current revision {0}, Transaction start revision {1}", object.getRevision(), startupRevision);
+                if (getLogger().isTraceEnabled()) {
+                    getLogger().tracev("Skipped cache. Current revision {0}, Transaction start revision {1}", object.getRevision(), startupRevision);
                 }
                 return;
             }
             if (rev.equals(object.getRevision())) {
-                if (id.endsWith("realm.clients")) RealmCacheManager.logger.tracev("adding Object.revision {0} rev {1}", object.getRevision(), rev);
                 cache.putForExternalRead(id, object);
                 return;
             }
             if (rev > object.getRevision()) { // revision is ahead, don't cache
-                if (id.endsWith("realm.clients")) RealmCacheManager.logger.trace("addRevisioned revision is ahead realm.clients");
+                if (getLogger().isTraceEnabled()) getLogger().tracev("Skipped cache. Object revision {0}, Cache revision {1}", object.getRevision(), rev);
                 return;
             }
             // revisions cache has a lower value than the object.revision, so update revision and add it to cache
-            if (id.endsWith("realm.clients")) RealmCacheManager.logger.tracev("adding Object.revision {0} rev {1}", object.getRevision(), rev);
             revisions.put(id, object.getRevision());
             if (lifespan < 0) cache.putForExternalRead(id, object);
             else cache.putForExternalRead(id, object, lifespan, TimeUnit.MILLISECONDS);
@@ -196,63 +197,36 @@ public abstract class CacheManager {
                 .filter(predicate).iterator();
     }
 
-    @CacheEntryInvalidated
-    public void cacheInvalidated(CacheEntryInvalidatedEvent<String, Object> event) {
-        if (event.isPre()) {
-            String key = event.getKey();
-            if (key.startsWith("invalidation.key")) {
-                // if you do cache.remove() on node 1 and the entry doesn't exist on node 2, node 2 never receives a invalidation event
-                // so, we do this to force this.
-                String bump = key.substring("invalidation.key".length());
-                RealmCacheManager.logger.tracev("bumping invalidation key {0}", bump);
-                bumpVersion(bump);
-                return;
-            }
 
-        } else {
-        //if (!event.isPre()) {
-            String key = event.getKey();
-            if (key.startsWith("invalidation.key")) {
-                // if you do cache.remove() on node 1 and the entry doesn't exist on node 2, node 2 never receives a invalidation event
-                // so, we do this to force this.
-                String bump = key.substring("invalidation.key".length());
-                bumpVersion(bump);
-                RealmCacheManager.logger.tracev("bumping invalidation key {0}", bump);
-                return;
-            }
-            bumpVersion(key);
-            Object object = event.getValue();
-            if (object != null) {
-                bumpVersion(key);
-                Predicate<Map.Entry<String, Revisioned>> predicate = getInvalidationPredicate(object);
-                if (predicate != null) runEvictions(predicate);
-                RealmCacheManager.logger.tracev("invalidating: {0}" + object.getClass().getName());
-            }
+    public void sendInvalidationEvents(KeycloakSession session, Collection<InvalidationEvent> invalidationEvents) {
+        ClusterProvider clusterProvider = session.getProvider(ClusterProvider.class);
+
+        // Maybe add InvalidationEvent, which will be collection of all invalidationEvents? That will reduce cluster traffic even more.
+        for (InvalidationEvent event : invalidationEvents) {
+            clusterProvider.notify(generateEventId(event), event, true);
         }
     }
 
-    @CacheEntriesEvicted
-    public void cacheEvicted(CacheEntriesEvictedEvent<String, Object> event) {
-        if (!event.isPre())
-        for (Map.Entry<String, Object> entry : event.getEntries().entrySet()) {
-            Object object = entry.getValue();
-            bumpVersion(entry.getKey());
-            if (object == null) continue;
-            RealmCacheManager.logger.tracev("evicting: {0}" + object.getClass().getName());
-            Predicate<Map.Entry<String, Revisioned>> predicate = getInvalidationPredicate(object);
-            if (predicate != null) runEvictions(predicate);
-        }
+    protected String generateEventId(InvalidationEvent event) {
+        return new StringBuilder(event.getId())
+                .append("_")
+                .append(event.hashCode())
+                .toString();
     }
 
-    public void runEvictions(Predicate<Map.Entry<String, Revisioned>> current) {
-        Set<String> evictions = new HashSet<>();
-        addInvalidations(current, evictions);
-        RealmCacheManager.logger.tracev("running evictions size: {0}", evictions.size());
-        for (String key : evictions) {
-            cache.evict(key);
-            bumpVersion(key);
+
+    protected void invalidationEventReceived(InvalidationEvent event) {
+        Set<String> invalidations = new HashSet<>();
+
+        addInvalidationsFromEvent(event, invalidations);
+
+        getLogger().debugf("Invalidating %d cache items after received event %s", invalidations.size(), event);
+
+        for (String invalidation : invalidations) {
+            invalidateObject(invalidation);
         }
     }
 
-    protected abstract Predicate<Map.Entry<String, Revisioned>> getInvalidationPredicate(Object object);
+    protected abstract void addInvalidationsFromEvent(InvalidationEvent event, Set<String> invalidations);
+
 }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java
index 980957a..11c1c62 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java
@@ -52,7 +52,7 @@ public class ClientAdapter implements ClientModel {
 
     private void getDelegateForUpdate() {
         if (updated == null) {
-            cacheSession.registerClientInvalidation(cached.getId());
+            cacheSession.registerClientInvalidation(cached.getId(), cached.getClientId(), cachedRealm.getId());
             updated = cacheSession.getDelegate().getClientById(cached.getId(), cachedRealm);
             if (updated == null) throw new IllegalStateException("Not found in database");
         }
@@ -577,18 +577,12 @@ public class ClientAdapter implements ClientModel {
 
     @Override
     public RoleModel addRole(String name) {
-        getDelegateForUpdate();
-        RoleModel role = updated.addRole(name);
-        cacheSession.registerRoleInvalidation(role.getId());
-        return role;
+        return cacheSession.addClientRole(getRealm(), this, name);
     }
 
     @Override
     public RoleModel addRole(String id, String name) {
-        getDelegateForUpdate();
-        RoleModel role =  updated.addRole(id, name);
-        cacheSession.registerRoleInvalidation(role.getId());
-        return role;
+        return cacheSession.addClientRole(getRealm(), this, id, name);
     }
 
     @Override
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java
index 4647f74..5dd4bac 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java
@@ -135,7 +135,6 @@ public class CachedRealm extends AbstractExtendableRevisioned {
     }
 
     protected List<String> defaultGroups = new LinkedList<String>();
-    protected Set<String> groups = new HashSet<String>();
     protected List<String> clientTemplates= new LinkedList<>();
     protected boolean internationalizationEnabled;
     protected Set<String> supportedLocales;
@@ -237,9 +236,7 @@ public class CachedRealm extends AbstractExtendableRevisioned {
                 executionsById.put(execution.getId(), execution);
             }
         }
-        for (GroupModel group : model.getGroups()) {
-            groups.add(group.getId());
-        }
+
         for (AuthenticatorConfigModel authenticator : model.getAuthenticatorConfigs()) {
             authenticatorConfigs.put(authenticator.getId(), authenticator);
         }
@@ -541,10 +538,6 @@ public class CachedRealm extends AbstractExtendableRevisioned {
         return clientAuthenticationFlow;
     }
 
-    public Set<String> getGroups() {
-        return groups;
-    }
-
     public List<String> getDefaultGroups() {
         return defaultGroups;
     }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/RoleListQuery.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/RoleListQuery.java
index 21a73ad..e924c05 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/RoleListQuery.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/RoleListQuery.java
@@ -59,7 +59,8 @@ public class RoleListQuery extends AbstractRevisioned implements RoleQuery, InCl
     public String toString() {
         return "RoleListQuery{" +
                 "id='" + getId() + "'" +
-                "realmName='" + realmName + '\'' +
+                ", realmName='" + realmName + '\'' +
+                ", clientUuid='" + client + '\'' +
                 '}';
     }
 }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientAddedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientAddedEvent.java
new file mode 100644
index 0000000..1b022ca
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientAddedEvent.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.cache.infinispan.events;
+
+import java.util.Set;
+
+import org.keycloak.models.cache.infinispan.RealmCacheManager;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ClientAddedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
+
+    private String clientUuid;
+    private String clientId;
+    private String realmId;
+
+    public static ClientAddedEvent create(String clientUuid, String clientId, String realmId) {
+        ClientAddedEvent event = new ClientAddedEvent();
+        event.clientUuid = clientUuid;
+        event.clientId = clientId;
+        event.realmId = realmId;
+        return event;
+    }
+
+    @Override
+    public String getId() {
+        return clientUuid;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("ClientAddedEvent [ realmId=%s, clientUuid=%s, clientId=%s ]", realmId, clientUuid, clientId);
+    }
+
+    @Override
+    public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
+        realmCache.clientAdded(realmId, clientUuid, clientId, invalidations);
+    }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientRemovedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientRemovedEvent.java
new file mode 100644
index 0000000..2e620db
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientRemovedEvent.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.cache.infinispan.events;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.cache.infinispan.RealmCacheManager;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ClientRemovedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
+
+    private String clientUuid;
+    private String clientId;
+    private String realmId;
+    // roleId -> roleName
+    private Map<String, String> clientRoles;
+
+    public static ClientRemovedEvent create(ClientModel client) {
+        ClientRemovedEvent event = new ClientRemovedEvent();
+
+        event.realmId = client.getRealm().getId();
+        event.clientUuid = client.getId();
+        event.clientId = client.getClientId();
+        event.clientRoles = new HashMap<>();
+        for (RoleModel clientRole : client.getRoles()) {
+            event.clientRoles.put(clientRole.getId(), clientRole.getName());
+        }
+
+        return event;
+    }
+
+    @Override
+    public String getId() {
+        return clientUuid;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("ClientRemovedEvent [ realmId=%s, clientUuid=%s, clientId=%s, clientRoleIds=%s ]", realmId, clientUuid, clientId, clientRoles);
+    }
+
+    @Override
+    public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
+        realmCache.clientRemoval(realmId, clientUuid, clientId, invalidations);
+
+        // Separate iteration for all client roles to invalidate records dependent on them
+        for (Map.Entry<String, String> clientRole : clientRoles.entrySet()) {
+            String roleId = clientRole.getKey();
+            String roleName = clientRole.getValue();
+            realmCache.roleRemoval(roleId, roleName, clientUuid, invalidations);
+        }
+    }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientTemplateEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientTemplateEvent.java
new file mode 100644
index 0000000..7bb13a9
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientTemplateEvent.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.cache.infinispan.events;
+
+import java.util.Set;
+
+import org.keycloak.models.cache.infinispan.RealmCacheManager;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ClientTemplateEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
+
+    private String clientTemplateId;
+
+    public static ClientTemplateEvent create(String clientTemplateId) {
+        ClientTemplateEvent event = new ClientTemplateEvent();
+        event.clientTemplateId = clientTemplateId;
+        return event;
+    }
+
+    @Override
+    public String getId() {
+        return clientTemplateId;
+    }
+
+
+    @Override
+    public String toString() {
+        return "ClientTemplateEvent [ " + clientTemplateId + " ]";
+    }
+
+    @Override
+    public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
+        // Nothing. ID was already invalidated
+    }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientUpdatedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientUpdatedEvent.java
new file mode 100644
index 0000000..cc6c263
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientUpdatedEvent.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.cache.infinispan.events;
+
+import java.util.Set;
+
+import org.keycloak.models.cache.infinispan.RealmCacheManager;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ClientUpdatedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
+
+    private String clientUuid;
+    private String clientId;
+    private String realmId;
+
+    public static ClientUpdatedEvent create(String clientUuid, String clientId, String realmId) {
+        ClientUpdatedEvent event = new ClientUpdatedEvent();
+        event.clientUuid = clientUuid;
+        event.clientId = clientId;
+        event.realmId = realmId;
+        return event;
+    }
+
+    @Override
+    public String getId() {
+        return clientUuid;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("ClientUpdatedEvent [ realmId=%s, clientUuid=%s, clientId=%s ]", realmId, clientUuid, clientId);
+    }
+
+    @Override
+    public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
+        realmCache.clientUpdated(realmId, clientUuid, clientId, invalidations);
+    }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupAddedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupAddedEvent.java
new file mode 100644
index 0000000..77dcf69
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupAddedEvent.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.cache.infinispan.events;
+
+import java.util.Set;
+
+import org.keycloak.models.cache.infinispan.RealmCacheManager;
+
+/**
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class GroupAddedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
+
+    private String groupId;
+    private String realmId;
+
+    public static GroupAddedEvent create(String groupId, String realmId) {
+        GroupAddedEvent event = new GroupAddedEvent();
+        event.realmId = realmId;
+        event.groupId = groupId;
+        return event;
+    }
+
+    @Override
+    public String getId() {
+        return groupId;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("GroupAddedEvent [ realmId=%s, groupId=%s ]", realmId, groupId);
+    }
+
+    @Override
+    public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
+        realmCache.groupQueriesInvalidations(realmId, invalidations);
+    }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupMovedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupMovedEvent.java
new file mode 100644
index 0000000..2f5566a
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupMovedEvent.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.cache.infinispan.events;
+
+import java.util.Set;
+
+import org.keycloak.models.GroupModel;
+import org.keycloak.models.cache.infinispan.RealmCacheManager;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class GroupMovedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
+
+    private String groupId;
+    private String newParentId; // null if moving to top-level
+    private String oldParentId; // null if moving from top-level
+    private String realmId;
+
+    public static GroupMovedEvent create(GroupModel group, GroupModel toParent, String realmId) {
+        GroupMovedEvent event = new GroupMovedEvent();
+        event.realmId = realmId;
+        event.groupId = group.getId();
+        event.oldParentId = group.getParentId();
+        event.newParentId = toParent==null ? null : toParent.getId();
+        return event;
+    }
+
+    @Override
+    public String getId() {
+        return groupId;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("GroupMovedEvent [ realmId=%s, groupId=%s, newParentId=%s, oldParentId=%s ]", realmId, groupId, newParentId, oldParentId);
+    }
+
+    @Override
+    public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
+        realmCache.groupQueriesInvalidations(realmId, invalidations);
+        if (newParentId != null) {
+            invalidations.add(newParentId);
+        }
+        if (oldParentId != null) {
+            invalidations.add(oldParentId);
+        }
+    }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupRemovedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupRemovedEvent.java
new file mode 100644
index 0000000..37689fa
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupRemovedEvent.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.cache.infinispan.events;
+
+import java.util.Set;
+
+import org.keycloak.models.GroupModel;
+import org.keycloak.models.cache.infinispan.RealmCacheManager;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class GroupRemovedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
+
+    private String groupId;
+    private String parentId;
+    private String realmId;
+
+    public static GroupRemovedEvent create(GroupModel group, String realmId) {
+        GroupRemovedEvent event = new GroupRemovedEvent();
+        event.realmId = realmId;
+        event.groupId = group.getId();
+        event.parentId = group.getParentId();
+        return event;
+    }
+
+    @Override
+    public String getId() {
+        return groupId;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("GroupRemovedEvent [ realmId=%s, groupId=%s, parentId=%s ]", realmId, groupId, parentId);
+    }
+
+    @Override
+    public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
+        realmCache.groupQueriesInvalidations(realmId, invalidations);
+        if (parentId != null) {
+            invalidations.add(parentId);
+        }
+    }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupUpdatedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupUpdatedEvent.java
new file mode 100644
index 0000000..c59021b
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupUpdatedEvent.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.cache.infinispan.events;
+
+import java.util.Set;
+
+import org.keycloak.models.cache.infinispan.RealmCacheManager;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class GroupUpdatedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
+
+    private String groupId;
+
+    public static GroupUpdatedEvent create(String groupId) {
+        GroupUpdatedEvent event = new GroupUpdatedEvent();
+        event.groupId = groupId;
+        return event;
+    }
+
+    @Override
+    public String getId() {
+        return groupId;
+    }
+
+
+    @Override
+    public String toString() {
+        return "GroupUpdatedEvent [ " + groupId + " ]";
+    }
+
+    @Override
+    public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
+        // Nothing. ID already invalidated
+    }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/InvalidationEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/InvalidationEvent.java
new file mode 100644
index 0000000..ea59ff5
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/InvalidationEvent.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.cache.infinispan.events;
+
+import org.keycloak.cluster.ClusterEvent;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public abstract class InvalidationEvent implements ClusterEvent {
+
+    public abstract String getId();
+
+    @Override
+    public int hashCode() {
+        return getClass().hashCode() * 13 + getId().hashCode();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) return false;
+        if (!obj.getClass().equals(this.getClass())) return false;
+
+        InvalidationEvent that = (InvalidationEvent) obj;
+        if (!that.getId().equals(getId())) return false;
+        return true;
+    }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmCacheInvalidationEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmCacheInvalidationEvent.java
new file mode 100644
index 0000000..2876e08
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmCacheInvalidationEvent.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.cache.infinispan.events;
+
+import java.util.Set;
+
+import org.keycloak.models.cache.infinispan.RealmCacheManager;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public interface RealmCacheInvalidationEvent {
+
+    void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations);
+
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmRemovedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmRemovedEvent.java
new file mode 100644
index 0000000..3558757
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmRemovedEvent.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.cache.infinispan.events;
+
+import java.util.Set;
+
+import org.keycloak.models.cache.infinispan.RealmCacheManager;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class RealmRemovedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
+
+    private String realmId;
+    private String realmName;
+
+    public static RealmRemovedEvent create(String realmId, String realmName) {
+        RealmRemovedEvent event = new RealmRemovedEvent();
+        event.realmId = realmId;
+        event.realmName = realmName;
+        return event;
+    }
+
+    @Override
+    public String getId() {
+        return realmId;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("RealmRemovedEvent [ realmId=%s, realmName=%s ]", realmId, realmName);
+    }
+
+    @Override
+    public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
+        realmCache.realmRemoval(realmId, realmName, invalidations);
+    }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmUpdatedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmUpdatedEvent.java
new file mode 100644
index 0000000..624fc6d
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmUpdatedEvent.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.cache.infinispan.events;
+
+import java.util.Set;
+
+import org.keycloak.models.cache.infinispan.RealmCacheManager;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class RealmUpdatedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
+
+    private String realmId;
+    private String realmName;
+
+    public static RealmUpdatedEvent create(String realmId, String realmName) {
+        RealmUpdatedEvent event = new RealmUpdatedEvent();
+        event.realmId = realmId;
+        event.realmName = realmName;
+        return event;
+    }
+
+    @Override
+    public String getId() {
+        return realmId;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("RealmUpdatedEvent [ realmId=%s, realmName=%s ]", realmId, realmName);
+    }
+
+    @Override
+    public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
+        realmCache.realmUpdated(realmId, realmName, invalidations);
+    }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleAddedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleAddedEvent.java
new file mode 100644
index 0000000..cb393e5
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleAddedEvent.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.cache.infinispan.events;
+
+import java.util.Set;
+
+import org.keycloak.models.cache.infinispan.RealmCacheManager;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class RoleAddedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
+
+    private String roleId;
+    private String containerId;
+
+    public static RoleAddedEvent create(String roleId, String containerId) {
+        RoleAddedEvent event = new RoleAddedEvent();
+        event.roleId = roleId;
+        event.containerId = containerId;
+        return event;
+    }
+
+    @Override
+    public String getId() {
+        return roleId;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("RoleAddedEvent [ roleId=%s, containerId=%s ]", roleId, containerId);
+    }
+
+    @Override
+    public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
+        realmCache.roleAdded(containerId, invalidations);
+    }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleRemovedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleRemovedEvent.java
new file mode 100644
index 0000000..6137b1b
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleRemovedEvent.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.cache.infinispan.events;
+
+import java.util.Set;
+
+import org.keycloak.models.cache.infinispan.RealmCacheManager;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class RoleRemovedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
+
+    private String roleId;
+    private String roleName;
+    private String containerId;
+
+    public static RoleRemovedEvent create(String roleId, String roleName, String containerId) {
+        RoleRemovedEvent event = new RoleRemovedEvent();
+        event.roleId = roleId;
+        event.roleName = roleName;
+        event.containerId = containerId;
+        return event;
+    }
+
+    @Override
+    public String getId() {
+        return roleId;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("RoleRemovedEvent [ roleId=%s, containerId=%s ]", roleId, containerId);
+    }
+
+    @Override
+    public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
+        realmCache.roleRemoval(roleId, roleName, containerId, invalidations);
+    }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleUpdatedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleUpdatedEvent.java
new file mode 100644
index 0000000..4b2ae5b
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleUpdatedEvent.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.cache.infinispan.events;
+
+import java.util.Set;
+
+import org.keycloak.models.cache.infinispan.RealmCacheManager;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class RoleUpdatedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
+
+    private String roleId;
+    private String roleName;
+    private String containerId;
+
+    public static RoleUpdatedEvent create(String roleId, String roleName, String containerId) {
+        RoleUpdatedEvent event = new RoleUpdatedEvent();
+        event.roleId = roleId;
+        event.roleName = roleName;
+        event.containerId = containerId;
+        return event;
+    }
+
+    @Override
+    public String getId() {
+        return roleId;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("RoleUpdatedEvent [ roleId=%s, roleName=%s, containerId=%s ]", roleId, roleName, containerId);
+    }
+
+    @Override
+    public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
+        realmCache.roleUpdated(containerId, roleName, invalidations);
+    }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserCacheInvalidationEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserCacheInvalidationEvent.java
new file mode 100644
index 0000000..964e97a
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserCacheInvalidationEvent.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.cache.infinispan.events;
+
+import java.util.Set;
+
+import org.keycloak.models.cache.infinispan.UserCacheManager;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public interface UserCacheInvalidationEvent {
+
+    void addInvalidations(UserCacheManager userCache, Set<String> invalidations);
+
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserCacheRealmInvalidationEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserCacheRealmInvalidationEvent.java
new file mode 100644
index 0000000..3996181
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserCacheRealmInvalidationEvent.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.cache.infinispan.events;
+
+import java.util.Set;
+
+import org.keycloak.models.cache.infinispan.UserCacheManager;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class UserCacheRealmInvalidationEvent  extends InvalidationEvent implements UserCacheInvalidationEvent {
+
+    private String realmId;
+
+    public static UserCacheRealmInvalidationEvent create(String realmId) {
+        UserCacheRealmInvalidationEvent event = new UserCacheRealmInvalidationEvent();
+        event.realmId = realmId;
+        return event;
+    }
+
+    @Override
+    public String getId() {
+        return realmId; // Just a placeholder
+    }
+
+    @Override
+    public String toString() {
+        return String.format("UserCacheRealmInvalidationEvent [ realmId=%s ]", realmId);
+    }
+
+    @Override
+    public void addInvalidations(UserCacheManager userCache, Set<String> invalidations) {
+        userCache.invalidateRealmUsers(realmId, invalidations);
+    }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserConsentsUpdatedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserConsentsUpdatedEvent.java
new file mode 100644
index 0000000..021e841
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserConsentsUpdatedEvent.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.cache.infinispan.events;
+
+import java.util.Set;
+
+import org.keycloak.models.cache.infinispan.UserCacheManager;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class UserConsentsUpdatedEvent extends InvalidationEvent implements UserCacheInvalidationEvent {
+
+    private String userId;
+
+    public static UserConsentsUpdatedEvent create(String userId) {
+        UserConsentsUpdatedEvent event = new UserConsentsUpdatedEvent();
+        event.userId = userId;
+        return event;
+    }
+
+    @Override
+    public String getId() {
+        return userId;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("UserConsentsUpdatedEvent [ userId=%s ]", userId);
+    }
+
+    @Override
+    public void addInvalidations(UserCacheManager userCache, Set<String> invalidations) {
+        userCache.consentInvalidation(userId, invalidations);
+    }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFederationLinkRemovedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFederationLinkRemovedEvent.java
new file mode 100644
index 0000000..15704df
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFederationLinkRemovedEvent.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.cache.infinispan.events;
+
+import java.util.Set;
+
+import org.keycloak.models.FederatedIdentityModel;
+import org.keycloak.models.cache.infinispan.UserCacheManager;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class UserFederationLinkRemovedEvent extends InvalidationEvent implements UserCacheInvalidationEvent {
+
+    private String userId;
+    private String realmId;
+    private String identityProviderId;
+    private String socialUserId;
+
+    public static UserFederationLinkRemovedEvent create(String userId, String realmId, FederatedIdentityModel socialLink) {
+        UserFederationLinkRemovedEvent event = new UserFederationLinkRemovedEvent();
+        event.userId = userId;
+        event.realmId = realmId;
+        if (socialLink != null) {
+            event.identityProviderId = socialLink.getIdentityProvider();
+            event.socialUserId = socialLink.getUserId();
+        }
+        return event;
+    }
+
+    @Override
+    public String getId() {
+        return userId;
+    }
+
+    public String getRealmId() {
+        return realmId;
+    }
+
+    public String getIdentityProviderId() {
+        return identityProviderId;
+    }
+
+    public String getSocialUserId() {
+        return socialUserId;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("UserFederationLinkRemovedEvent [ userId=%s, identityProviderId=%s, socialUserId=%s ]", userId, identityProviderId, socialUserId);
+    }
+
+    @Override
+    public void addInvalidations(UserCacheManager userCache, Set<String> invalidations) {
+        userCache.federatedIdentityLinkRemovedInvalidation(userId, realmId, identityProviderId, socialUserId, invalidations);
+    }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFederationLinkUpdatedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFederationLinkUpdatedEvent.java
new file mode 100644
index 0000000..8bbfb41
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFederationLinkUpdatedEvent.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.models.cache.infinispan.events;
+
+import java.util.Set;
+
+import org.keycloak.models.cache.infinispan.UserCacheManager;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class UserFederationLinkUpdatedEvent extends InvalidationEvent implements UserCacheInvalidationEvent {
+
+    private String userId;
+
+    public static UserFederationLinkUpdatedEvent create(String userId) {
+        UserFederationLinkUpdatedEvent event = new UserFederationLinkUpdatedEvent();
+        event.userId = userId;
+        return event;
+    }
+
+    @Override
+    public String getId() {
+        return userId;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("UserFederationLinkUpdatedEvent [ userId=%s ]", userId);
+    }
+
+    @Override
+    public void addInvalidations(UserCacheManager userCache, Set<String> invalidations) {
+        userCache.federatedIdentityLinkUpdatedInvalidation(userId, invalidations);
+    }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFullInvalidationEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFullInvalidationEvent.java
new file mode 100644
index 0000000..d637ac2
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFullInvalidationEvent.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.cache.infinispan.events;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.keycloak.models.FederatedIdentityModel;
+import org.keycloak.models.cache.infinispan.UserCacheManager;
+
+/**
+ * Used when user added/removed
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class UserFullInvalidationEvent extends InvalidationEvent implements UserCacheInvalidationEvent {
+
+    private String userId;
+    private String username;
+    private String email;
+    private String realmId;
+    private boolean identityFederationEnabled;
+    private Map<String, String> federatedIdentities;
+
+    public static UserFullInvalidationEvent create(String userId, String username, String email, String realmId, boolean identityFederationEnabled, Collection<FederatedIdentityModel> federatedIdentities) {
+        UserFullInvalidationEvent event = new UserFullInvalidationEvent();
+        event.userId = userId;
+        event.username = username;
+        event.email = email;
+        event.realmId = realmId;
+
+        event.identityFederationEnabled = identityFederationEnabled;
+        if (identityFederationEnabled) {
+            event.federatedIdentities = new HashMap<>();
+            for (FederatedIdentityModel socialLink : federatedIdentities) {
+                event.federatedIdentities.put(socialLink.getIdentityProvider(), socialLink.getUserId());
+            }
+        }
+
+        return event;
+    }
+
+    @Override
+    public String getId() {
+        return userId;
+    }
+
+    public Map<String, String> getFederatedIdentities() {
+        return federatedIdentities;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("UserFullInvalidationEvent [ userId=%s, username=%s, email=%s ]", userId, username, email);
+    }
+
+    @Override
+    public void addInvalidations(UserCacheManager userCache, Set<String> invalidations) {
+        userCache.fullUserInvalidation(userId, username, email, realmId, identityFederationEnabled, federatedIdentities, invalidations);
+    }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserUpdatedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserUpdatedEvent.java
new file mode 100644
index 0000000..429b4af
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserUpdatedEvent.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.cache.infinispan.events;
+
+import java.util.Set;
+
+import org.keycloak.models.cache.infinispan.UserCacheManager;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class UserUpdatedEvent extends InvalidationEvent implements UserCacheInvalidationEvent {
+
+    private String userId;
+    private String username;
+    private String email;
+    private String realmId;
+
+    public static UserUpdatedEvent create(String userId, String username, String email, String realmId) {
+        UserUpdatedEvent event = new UserUpdatedEvent();
+        event.userId = userId;
+        event.username = username;
+        event.email = email;
+        event.realmId = realmId;
+        return event;
+    }
+
+    @Override
+    public String getId() {
+        return userId;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("UserUpdatedEvent [ userId=%s, username=%s, email=%s ]", userId, username, email);
+    }
+
+    @Override
+    public void addInvalidations(UserCacheManager userCache, Set<String> invalidations) {
+        userCache.userUpdatedInvalidations(userId, username, email, realmId, invalidations);
+    }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheRealmProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheRealmProviderFactory.java
index 4bbe4c7..c2ad8ce 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheRealmProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheRealmProviderFactory.java
@@ -21,7 +21,6 @@ import org.infinispan.Cache;
 import org.jboss.logging.Logger;
 import org.keycloak.Config;
 import org.keycloak.cluster.ClusterEvent;
-import org.keycloak.cluster.ClusterListener;
 import org.keycloak.cluster.ClusterProvider;
 import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
 import org.keycloak.models.KeycloakSession;
@@ -29,6 +28,7 @@ import org.keycloak.models.KeycloakSessionFactory;
 import org.keycloak.models.cache.CacheRealmProvider;
 import org.keycloak.models.cache.CacheRealmProviderFactory;
 import org.keycloak.models.cache.infinispan.entities.Revisioned;
+import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
 
 /**
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -54,14 +54,23 @@ public class InfinispanCacheRealmProviderFactory implements CacheRealmProviderFa
                     Cache<String, Revisioned> cache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.REALM_CACHE_NAME);
                     Cache<String, Long> revisions = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.REALM_REVISIONS_CACHE_NAME);
                     realmCache = new RealmCacheManager(cache, revisions);
+
                     ClusterProvider cluster = session.getProvider(ClusterProvider.class);
-                    cluster.registerListener(REALM_CLEAR_CACHE_EVENTS, new ClusterListener() {
-                        @Override
-                        public void run(ClusterEvent event) {
-                            realmCache.clear();
+                    cluster.registerListener(ClusterProvider.ALL, (ClusterEvent event) -> {
+
+                        if (event instanceof InvalidationEvent) {
+                            InvalidationEvent invalidationEvent = (InvalidationEvent) event;
+                            realmCache.invalidationEventReceived(invalidationEvent);
                         }
                     });
 
+                    cluster.registerListener(REALM_CLEAR_CACHE_EVENTS, (ClusterEvent event) -> {
+
+                        realmCache.clear();
+
+                    });
+
+                    log.debug("Registered cluster listeners");
                 }
             }
         }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanUserCacheProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanUserCacheProviderFactory.java
index 14d420b..e8c2ba1 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanUserCacheProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanUserCacheProviderFactory.java
@@ -21,7 +21,6 @@ import org.infinispan.Cache;
 import org.jboss.logging.Logger;
 import org.keycloak.Config;
 import org.keycloak.cluster.ClusterEvent;
-import org.keycloak.cluster.ClusterListener;
 import org.keycloak.cluster.ClusterProvider;
 import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
 import org.keycloak.models.KeycloakSession;
@@ -29,6 +28,7 @@ import org.keycloak.models.KeycloakSessionFactory;
 import org.keycloak.models.cache.UserCache;
 import org.keycloak.models.cache.UserCacheProviderFactory;
 import org.keycloak.models.cache.infinispan.entities.Revisioned;
+import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -55,13 +55,25 @@ public class InfinispanUserCacheProviderFactory implements UserCacheProviderFact
                     Cache<String, Revisioned> cache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.USER_CACHE_NAME);
                     Cache<String, Long> revisions = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.USER_REVISIONS_CACHE_NAME);
                     userCache = new UserCacheManager(cache, revisions);
+
                     ClusterProvider cluster = session.getProvider(ClusterProvider.class);
-                    cluster.registerListener(USER_CLEAR_CACHE_EVENTS, new ClusterListener() {
-                        @Override
-                        public void run(ClusterEvent event) {
-                            userCache.clear();
+
+                    cluster.registerListener(ClusterProvider.ALL, (ClusterEvent event) -> {
+
+                        if (event instanceof InvalidationEvent) {
+                            InvalidationEvent invalidationEvent = (InvalidationEvent) event;
+                            userCache.invalidationEventReceived(invalidationEvent);
                         }
+
                     });
+
+                    cluster.registerListener(USER_CLEAR_CACHE_EVENTS, (ClusterEvent event) -> {
+
+                        userCache.clear();
+
+                    });
+
+                    log.debug("Registered cluster listeners");
                 }
             }
         }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
index 069b34a..1748e3c 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
@@ -39,13 +39,8 @@ import org.keycloak.models.UserFederationMapperModel;
 import org.keycloak.models.UserFederationProviderModel;
 import org.keycloak.models.cache.CachedRealmModel;
 import org.keycloak.models.cache.infinispan.entities.CachedRealm;
-import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.storage.UserStorageProvider;
 
-import java.security.Key;
-import java.security.PrivateKey;
-import java.security.PublicKey;
-import java.security.cert.X509Certificate;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashSet;
@@ -75,7 +70,7 @@ public class RealmAdapter implements CachedRealmModel {
     @Override
     public RealmModel getDelegateForUpdate() {
         if (updated == null) {
-            cacheSession.registerRealmInvalidation(cached.getId());
+            cacheSession.registerRealmInvalidation(cached.getId(), cached.getName());
             updated = cacheSession.getDelegate().getRealm(cached.getId());
             if (updated == null) throw new IllegalStateException("Not found in database");
         }
@@ -732,13 +727,6 @@ public class RealmAdapter implements CachedRealmModel {
     }
 
     @Override
-    public boolean removeRoleById(String id) {
-        cacheSession.registerRoleInvalidation(id);
-        getDelegateForUpdate();
-        return updated.removeRoleById(id);
-    }
-
-    @Override
     public boolean isEventsEnabled() {
         if (isUpdated()) return updated.isEventsEnabled();
         return cached.isEventsEnabled();
@@ -837,18 +825,12 @@ public class RealmAdapter implements CachedRealmModel {
 
     @Override
     public RoleModel addRole(String name) {
-        getDelegateForUpdate();
-        RoleModel role = updated.addRole(name);
-        cacheSession.registerRoleInvalidation(role.getId());
-        return role;
+        return cacheSession.addRealmRole(this, name);
     }
 
     @Override
     public RoleModel addRole(String id, String name) {
-        getDelegateForUpdate();
-        RoleModel role =  updated.addRole(id, name);
-        cacheSession.registerRoleInvalidation(role.getId());
-        return role;
+        return cacheSession.addRealmRole(this, id, name);
     }
 
     @Override
@@ -1258,12 +1240,6 @@ public class RealmAdapter implements CachedRealmModel {
     }
 
     @Override
-    public void addTopLevelGroup(GroupModel subGroup) {
-        cacheSession.addTopLevelGroup(this, subGroup);
-
-    }
-
-    @Override
     public void moveGroup(GroupModel group, GroupModel toParent) {
         cacheSession.moveGroup(this, group, toParent);
     }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheManager.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheManager.java
index 55e3b38..b01dbab 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheManager.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheManager.java
@@ -18,145 +18,88 @@
 package org.keycloak.models.cache.infinispan;
 
 import org.infinispan.Cache;
-import org.infinispan.notifications.Listener;
 import org.jboss.logging.Logger;
-import org.keycloak.models.cache.infinispan.entities.CachedClient;
-import org.keycloak.models.cache.infinispan.entities.CachedClientTemplate;
-import org.keycloak.models.cache.infinispan.entities.CachedGroup;
-import org.keycloak.models.cache.infinispan.entities.CachedRealm;
-import org.keycloak.models.cache.infinispan.entities.CachedRole;
+import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
 import org.keycloak.models.cache.infinispan.entities.Revisioned;
-import org.keycloak.models.cache.infinispan.stream.ClientQueryPredicate;
-import org.keycloak.models.cache.infinispan.stream.ClientTemplateQueryPredicate;
-import org.keycloak.models.cache.infinispan.stream.GroupQueryPredicate;
+import org.keycloak.models.cache.infinispan.events.RealmCacheInvalidationEvent;
 import org.keycloak.models.cache.infinispan.stream.HasRolePredicate;
 import org.keycloak.models.cache.infinispan.stream.InClientPredicate;
 import org.keycloak.models.cache.infinispan.stream.InRealmPredicate;
-import org.keycloak.models.cache.infinispan.stream.RealmQueryPredicate;
 
-import java.util.Map;
 import java.util.Set;
-import java.util.function.Predicate;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
  */
-@Listener
 public class RealmCacheManager extends CacheManager {
 
-    protected static final Logger logger = Logger.getLogger(RealmCacheManager.class);
-
-    public RealmCacheManager(Cache<String, Revisioned> cache, Cache<String, Long> revisions) {
-        super(cache, revisions);
-    }
-
-
-    public void realmInvalidation(String id, Set<String> invalidations) {
-        Predicate<Map.Entry<String, Revisioned>> predicate = getRealmInvalidationPredicate(id);
-        addInvalidations(predicate, invalidations);
-    }
-
-    public Predicate<Map.Entry<String, Revisioned>> getRealmInvalidationPredicate(String id) {
-        return RealmQueryPredicate.create().realm(id);
-    }
-
-    public void clientInvalidation(String id, Set<String> invalidations) {
-        addInvalidations(getClientInvalidationPredicate(id), invalidations);
-    }
-
-    public Predicate<Map.Entry<String, Revisioned>> getClientInvalidationPredicate(String id) {
-        return ClientQueryPredicate.create().client(id);
-    }
-
-    public void roleInvalidation(String id, Set<String> invalidations) {
-        addInvalidations(getRoleInvalidationPredicate(id), invalidations);
+    private static final Logger logger = Logger.getLogger(RealmCacheManager.class);
 
+    @Override
+    protected Logger getLogger() {
+        return logger;
     }
 
-    public Predicate<Map.Entry<String, Revisioned>> getRoleInvalidationPredicate(String id) {
-        return HasRolePredicate.create().role(id);
+    public RealmCacheManager(Cache<String, Revisioned> cache, Cache<String, Long> revisions) {
+        super(cache, revisions);
     }
 
-    public void groupInvalidation(String id, Set<String> invalidations) {
-        addInvalidations(getGroupInvalidationPredicate(id), invalidations);
 
+    public void realmUpdated(String id, String name, Set<String> invalidations) {
+        invalidations.add(id);
+        invalidations.add(RealmCacheSession.getRealmByNameCacheKey(name));
     }
 
-    public Predicate<Map.Entry<String, Revisioned>> getGroupInvalidationPredicate(String id) {
-        return GroupQueryPredicate.create().group(id);
-    }
-
-    public void clientTemplateInvalidation(String id, Set<String> invalidations) {
-        addInvalidations(getClientTemplateInvalidationPredicate(id), invalidations);
+    public void realmRemoval(String id, String name, Set<String> invalidations) {
+        realmUpdated(id, name, invalidations);
 
+        addInvalidations(InRealmPredicate.create().realm(id), invalidations);
     }
 
-    public Predicate<Map.Entry<String, Revisioned>> getClientTemplateInvalidationPredicate(String id) {
-        return ClientTemplateQueryPredicate.create().template(id);
+    public void roleAdded(String roleContainerId, Set<String> invalidations) {
+        invalidations.add(RealmCacheSession.getRolesCacheKey(roleContainerId));
     }
 
-    public void realmRemoval(String id, Set<String> invalidations) {
-        Predicate<Map.Entry<String, Revisioned>> predicate = getRealmRemovalPredicate(id);
-        addInvalidations(predicate, invalidations);
+    public void roleUpdated(String roleContainerId, String roleName, Set<String> invalidations) {
+        invalidations.add(RealmCacheSession.getRoleByNameCacheKey(roleContainerId, roleName));
     }
 
-    public Predicate<Map.Entry<String, Revisioned>> getRealmRemovalPredicate(String id) {
-        Predicate<Map.Entry<String, Revisioned>> predicate = null;
-        predicate = RealmQueryPredicate.create().realm(id)
-                .or(InRealmPredicate.create().realm(id));
-        return predicate;
-    }
+    public void roleRemoval(String id, String roleName, String roleContainerId, Set<String> invalidations) {
+        invalidations.add(RealmCacheSession.getRolesCacheKey(roleContainerId));
+        invalidations.add(RealmCacheSession.getRoleByNameCacheKey(roleContainerId, roleName));
 
-    public void clientAdded(String realmId, String id, Set<String> invalidations) {
-        addInvalidations(getClientAddedPredicate(realmId), invalidations);
+        addInvalidations(HasRolePredicate.create().role(id), invalidations);
     }
 
-    public Predicate<Map.Entry<String, Revisioned>> getClientAddedPredicate(String realmId) {
-        return ClientQueryPredicate.create().inRealm(realmId);
+    public void groupQueriesInvalidations(String realmId, Set<String> invalidations) {
+        invalidations.add(RealmCacheSession.getGroupsQueryCacheKey(realmId));
+        invalidations.add(RealmCacheSession.getTopGroupsQueryCacheKey(realmId)); // Just easier to always invalidate top-level too. It's not big performance penalty
     }
 
-    public void clientRemoval(String realmId, String id, Set<String> invalidations) {
-        Predicate<Map.Entry<String, Revisioned>> predicate = null;
-        predicate = getClientRemovalPredicate(realmId, id);
-        addInvalidations(predicate, invalidations);
+    public void clientAdded(String realmId, String clientUUID, String clientId, Set<String> invalidations) {
+        invalidations.add(RealmCacheSession.getRealmClientsQueryCacheKey(realmId));
     }
 
-    public Predicate<Map.Entry<String, Revisioned>> getClientRemovalPredicate(String realmId, String id) {
-        Predicate<Map.Entry<String, Revisioned>> predicate;
-        predicate = ClientQueryPredicate.create().inRealm(realmId)
-                .or(ClientQueryPredicate.create().client(id))
-                .or(InClientPredicate.create().client(id));
-        return predicate;
+    public void clientUpdated(String realmId, String clientUuid, String clientId, Set<String> invalidations) {
+        invalidations.add(RealmCacheSession.getClientByClientIdCacheKey(clientId, realmId));
     }
 
-    public void roleRemoval(String id, Set<String> invalidations) {
-        addInvalidations(getRoleRemovalPredicate(id), invalidations);
+    // Client roles invalidated separately
+    public void clientRemoval(String realmId, String clientUUID, String clientId, Set<String> invalidations) {
+        invalidations.add(RealmCacheSession.getRealmClientsQueryCacheKey(realmId));
+        invalidations.add(RealmCacheSession.getClientByClientIdCacheKey(clientId, realmId));
 
+        addInvalidations(InClientPredicate.create().client(clientUUID), invalidations);
     }
 
-    public Predicate<Map.Entry<String, Revisioned>> getRoleRemovalPredicate(String id) {
-        return getRoleInvalidationPredicate(id);
-    }
 
     @Override
-    protected Predicate<Map.Entry<String, Revisioned>> getInvalidationPredicate(Object object) {
-        if (object instanceof CachedRealm) {
-            CachedRealm cached = (CachedRealm)object;
-            return getRealmRemovalPredicate(cached.getId());
-        } else if (object instanceof CachedClient) {
-            CachedClient cached = (CachedClient)object;
-            Predicate<Map.Entry<String, Revisioned>> predicate = getClientRemovalPredicate(cached.getRealm(), cached.getId());
-            return predicate;
-        } else if (object instanceof CachedRole) {
-            CachedRole cached = (CachedRole)object;
-            return getRoleRemovalPredicate(cached.getId());
-        } else if (object instanceof CachedGroup) {
-            CachedGroup cached = (CachedGroup)object;
-            return getGroupInvalidationPredicate(cached.getId());
-        } else if (object instanceof CachedClientTemplate) {
-            CachedClientTemplate cached = (CachedClientTemplate)object;
-            return getClientTemplateInvalidationPredicate(cached.getId());
+    protected void addInvalidationsFromEvent(InvalidationEvent event, Set<String> invalidations) {
+        if (event instanceof RealmCacheInvalidationEvent) {
+            invalidations.add(event.getId());
+
+            ((RealmCacheInvalidationEvent) event).addInvalidations(this, invalidations);
         }
-        return null;
     }
+
 }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java
index 9321f47..d61a611 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java
@@ -19,6 +19,7 @@ package org.keycloak.models.cache.infinispan;
 
 import org.jboss.logging.Logger;
 import org.keycloak.cluster.ClusterProvider;
+import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
 import org.keycloak.migration.MigrationModel;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.ClientTemplateModel;
@@ -38,8 +39,22 @@ import org.keycloak.models.cache.infinispan.entities.CachedRealm;
 import org.keycloak.models.cache.infinispan.entities.CachedRealmRole;
 import org.keycloak.models.cache.infinispan.entities.CachedRole;
 import org.keycloak.models.cache.infinispan.entities.ClientListQuery;
+import org.keycloak.models.cache.infinispan.entities.GroupListQuery;
 import org.keycloak.models.cache.infinispan.entities.RealmListQuery;
 import org.keycloak.models.cache.infinispan.entities.RoleListQuery;
+import org.keycloak.models.cache.infinispan.events.ClientAddedEvent;
+import org.keycloak.models.cache.infinispan.events.ClientRemovedEvent;
+import org.keycloak.models.cache.infinispan.events.ClientTemplateEvent;
+import org.keycloak.models.cache.infinispan.events.ClientUpdatedEvent;
+import org.keycloak.models.cache.infinispan.events.GroupAddedEvent;
+import org.keycloak.models.cache.infinispan.events.GroupMovedEvent;
+import org.keycloak.models.cache.infinispan.events.GroupRemovedEvent;
+import org.keycloak.models.cache.infinispan.events.GroupUpdatedEvent;
+import org.keycloak.models.cache.infinispan.events.RealmRemovedEvent;
+import org.keycloak.models.cache.infinispan.events.RealmUpdatedEvent;
+import org.keycloak.models.cache.infinispan.events.RoleAddedEvent;
+import org.keycloak.models.cache.infinispan.events.RoleRemovedEvent;
+import org.keycloak.models.cache.infinispan.events.RoleUpdatedEvent;
 import org.keycloak.models.utils.KeycloakModelUtils;
 
 import java.util.HashMap;
@@ -126,6 +141,7 @@ public class RealmCacheSession implements CacheRealmProvider {
     protected Map<String, GroupAdapter> managedGroups = new HashMap<>();
     protected Set<String> listInvalidations = new HashSet<>();
     protected Set<String> invalidations = new HashSet<>();
+    protected Set<InvalidationEvent> invalidationEvents = new HashSet<>(); // Events to be sent across cluster
 
     protected boolean clearAll;
     protected final long startupRevision;
@@ -150,7 +166,7 @@ public class RealmCacheSession implements CacheRealmProvider {
     public void clear() {
         cache.clear();
         ClusterProvider cluster = session.getProvider(ClusterProvider.class);
-        cluster.notify(InfinispanCacheRealmProviderFactory.REALM_CLEAR_CACHE_EVENTS, new ClearCacheEvent());
+        cluster.notify(InfinispanCacheRealmProviderFactory.REALM_CLEAR_CACHE_EVENTS, new ClearCacheEvent(), true);
     }
 
     @Override
@@ -167,21 +183,19 @@ public class RealmCacheSession implements CacheRealmProvider {
     }
 
     @Override
-    public void registerRealmInvalidation(String id) {
-        invalidateRealm(id);
-        cache.realmInvalidation(id, invalidations);
-    }
-
-    private void invalidateRealm(String id) {
-        invalidations.add(id);
+    public void registerRealmInvalidation(String id, String name) {
+        cache.realmUpdated(id, name, invalidations);
         RealmAdapter adapter = managedRealms.get(id);
         if (adapter != null) adapter.invalidateFlag();
+
+        invalidationEvents.add(RealmUpdatedEvent.create(id, name));
     }
 
     @Override
-    public void registerClientInvalidation(String id) {
+    public void registerClientInvalidation(String id, String clientId, String realmId) {
         invalidateClient(id);
-        cache.clientInvalidation(id, invalidations);
+        invalidationEvents.add(ClientUpdatedEvent.create(id, clientId, realmId));
+        cache.clientUpdated(realmId, id, clientId, invalidations);
     }
 
     private void invalidateClient(String id) {
@@ -193,7 +207,9 @@ public class RealmCacheSession implements CacheRealmProvider {
     @Override
     public void registerClientTemplateInvalidation(String id) {
         invalidateClientTemplate(id);
-        cache.clientTemplateInvalidation(id, invalidations);
+        // Note: Adding/Removing client template is supposed to invalidate CachedRealm as well, so the list of clientTemplates is invalidated.
+        // But separate RealmUpdatedEvent will be sent for it. So ClientTemplateEvent don't need to take care of it.
+        invalidationEvents.add(ClientTemplateEvent.create(id));
     }
 
     private void invalidateClientTemplate(String id) {
@@ -203,14 +219,15 @@ public class RealmCacheSession implements CacheRealmProvider {
     }
 
     @Override
-    public void registerRoleInvalidation(String id) {
+    public void registerRoleInvalidation(String id, String roleName, String roleContainerId) {
         invalidateRole(id);
-        roleInvalidations(id);
+        cache.roleUpdated(roleContainerId, roleName, invalidations);
+        invalidationEvents.add(RoleUpdatedEvent.create(id, roleName, roleContainerId));
     }
 
-    private void roleInvalidations(String roleId) {
+    private void roleRemovalInvalidations(String roleId, String roleName, String roleContainerId) {
         Set<String> newInvalidations = new HashSet<>();
-        cache.roleInvalidation(roleId, newInvalidations);
+        cache.roleRemoval(roleId, roleName, roleContainerId, newInvalidations);
         invalidations.addAll(newInvalidations);
         // need to make sure that scope and group mapping clients and groups are invalidated
         for (String id : newInvalidations) {
@@ -229,6 +246,11 @@ public class RealmCacheSession implements CacheRealmProvider {
                 clientTemplate.invalidate();
                 continue;
             }
+            RoleAdapter role = managedRoles.get(id);
+            if (role != null) {
+                role.invalidate();
+                continue;
+            }
 
 
         }
@@ -243,10 +265,26 @@ public class RealmCacheSession implements CacheRealmProvider {
         if (adapter != null) adapter.invalidate();
     }
 
+    private void addedRole(String roleId, String roleContainerId) {
+        // this is needed so that a new role that hasn't been committed isn't cached in a query
+        listInvalidations.add(roleContainerId);
+
+        invalidateRole(roleId);
+        cache.roleAdded(roleContainerId, invalidations);
+        invalidationEvents.add(RoleAddedEvent.create(roleId, roleContainerId));
+    }
+
     @Override
     public void registerGroupInvalidation(String id) {
+        invalidateGroup(id, null, false);
+        addGroupEventIfAbsent(GroupUpdatedEvent.create(id));
+    }
+
+    private void invalidateGroup(String id, String realmId, boolean invalidateQueries) {
         invalidateGroup(id);
-        cache.groupInvalidation(id, invalidations);
+        if (invalidateQueries) {
+            cache.groupQueriesInvalidations(realmId, invalidations);
+        }
     }
 
     private void invalidateGroup(String id) {
@@ -259,6 +297,8 @@ public class RealmCacheSession implements CacheRealmProvider {
         for (String id : invalidations) {
             cache.invalidateObject(id);
         }
+
+        cache.sendInvalidationEvents(session, invalidationEvents);
     }
 
     private KeycloakTransaction getPrepareTransaction() {
@@ -358,14 +398,14 @@ public class RealmCacheSession implements CacheRealmProvider {
     @Override
     public RealmModel createRealm(String name) {
         RealmModel realm = getDelegate().createRealm(name);
-        registerRealmInvalidation(realm.getId());
+        registerRealmInvalidation(realm.getId(), realm.getName());
         return realm;
     }
 
     @Override
     public RealmModel createRealm(String id, String name) {
         RealmModel realm =  getDelegate().createRealm(id, name);
-        registerRealmInvalidation(realm.getId());
+        registerRealmInvalidation(realm.getId(), realm.getName());
         return realm;
     }
 
@@ -434,7 +474,7 @@ public class RealmCacheSession implements CacheRealmProvider {
         }
     }
 
-    public String getRealmByNameCacheKey(String name) {
+    static String getRealmByNameCacheKey(String name) {
         return "realm.query.by.name." + name;
     }
 
@@ -457,20 +497,12 @@ public class RealmCacheSession implements CacheRealmProvider {
         RealmModel realm = getRealm(id);
         if (realm == null) return false;
 
-        invalidations.add(getRealmClientsQueryCacheKey(id));
-        invalidations.add(getRealmByNameCacheKey(realm.getName()));
         cache.invalidateObject(id);
-        cache.realmRemoval(id, invalidations);
+        invalidationEvents.add(RealmRemovedEvent.create(id, realm.getName()));
+        cache.realmRemoval(id, realm.getName(), invalidations);
         return getDelegate().removeRealm(id);
     }
 
-    protected void invalidateClient(RealmModel realm, ClientModel client) {
-        invalidateClient(client.getId());
-        invalidations.add(getRealmClientsQueryCacheKey(realm.getId()));
-        invalidations.add(getClientByClientIdCacheKey(client.getClientId(), realm));
-        listInvalidations.add(realm.getId());
-    }
-
 
     @Override
     public ClientModel addClient(RealmModel realm, String clientId) {
@@ -486,30 +518,32 @@ public class RealmCacheSession implements CacheRealmProvider {
 
     private ClientModel addedClient(RealmModel realm, ClientModel client) {
         logger.trace("added Client.....");
-        // need to invalidate realm client query cache every time as it may not be loaded on this node, but loaded on another
-        invalidateClient(realm, client);
-        cache.clientAdded(realm.getId(), client.getId(), invalidations);
-        // this is needed so that a new client that hasn't been committed isn't cached in a query
+
+        invalidateClient(client.getId());
+        // this is needed so that a client that hasn't been committed isn't cached in a query
         listInvalidations.add(realm.getId());
+
+        invalidationEvents.add(ClientAddedEvent.create(client.getId(), client.getClientId(), realm.getId()));
+        cache.clientAdded(realm.getId(), client.getId(), client.getClientId(), invalidations);
         return client;
     }
 
-    private String getRealmClientsQueryCacheKey(String realm) {
+    static String getRealmClientsQueryCacheKey(String realm) {
         return realm + REALM_CLIENTS_QUERY_SUFFIX;
     }
 
-    private String getGroupsQueryCacheKey(String realm) {
+    static String getGroupsQueryCacheKey(String realm) {
         return realm + ".groups";
     }
 
-    private String getTopGroupsQueryCacheKey(String realm) {
+    static String getTopGroupsQueryCacheKey(String realm) {
         return realm + ".top.groups";
     }
 
-    private String getRolesCacheKey(String container) {
+    static String getRolesCacheKey(String container) {
         return container + ROLES_QUERY_SUFFIX;
     }
-    private String getRoleByNameCacheKey(String container, String name) {
+    static String getRoleByNameCacheKey(String container, String name) {
         return container + "." + name + ROLES_QUERY_SUFFIX;
     }
 
@@ -541,6 +575,7 @@ public class RealmCacheSession implements CacheRealmProvider {
         for (String id : query.getClients()) {
             ClientModel client = session.realms().getClientById(id, realm);
             if (client == null) {
+                // TODO: Handle with cluster invalidations too
                 invalidations.add(cacheKey);
                 return getDelegate().getClients(realm);
             }
@@ -554,12 +589,16 @@ public class RealmCacheSession implements CacheRealmProvider {
     public boolean removeClient(String id, RealmModel realm) {
         ClientModel client = getClientById(id, realm);
         if (client == null) return false;
-        // need to invalidate realm client query cache every time client list is changed
-        invalidateClient(realm, client);
-        cache.clientRemoval(realm.getId(), id, invalidations);
+
+        invalidateClient(client.getId());
+        // this is needed so that a client that hasn't been committed isn't cached in a query
+        listInvalidations.add(realm.getId());
+
+        invalidationEvents.add(ClientRemovedEvent.create(client));
+        cache.clientRemoval(realm.getId(), id, client.getClientId(), invalidations);
+
         for (RoleModel role : client.getRoles()) {
-            String roleId = role.getId();
-            roleInvalidations(roleId);
+            roleRemovalInvalidations(role.getId(), role.getName(), client.getId());
         }
         return getDelegate().removeClient(id, realm);
     }
@@ -577,11 +616,8 @@ public class RealmCacheSession implements CacheRealmProvider {
 
     @Override
     public RoleModel addRealmRole(RealmModel realm, String id, String name) {
-        invalidations.add(getRolesCacheKey(realm.getId()));
-        // this is needed so that a new role that hasn't been committed isn't cached in a query
-        listInvalidations.add(realm.getId());
         RoleModel role = getDelegate().addRealmRole(realm, name);
-        invalidations.add(role.getId());
+        addedRole(role.getId(), realm.getId());
         return role;
     }
 
@@ -664,11 +700,8 @@ public class RealmCacheSession implements CacheRealmProvider {
 
     @Override
     public RoleModel addClientRole(RealmModel realm, ClientModel client, String id, String name) {
-        invalidations.add(getRolesCacheKey(client.getId()));
-        // this is needed so that a new role that hasn't been committed isn't cached in a query
-        listInvalidations.add(client.getId());
         RoleModel role = getDelegate().addClientRole(realm, client, id, name);
-        invalidateRole(role.getId());
+        addedRole(role.getId(), client.getId());
         return role;
     }
 
@@ -734,10 +767,12 @@ public class RealmCacheSession implements CacheRealmProvider {
 
     @Override
     public boolean removeRole(RealmModel realm, RoleModel role) {
-        invalidations.add(getRolesCacheKey(role.getContainer().getId()));
-        invalidations.add(getRoleByNameCacheKey(role.getContainer().getId(), role.getName()));
         listInvalidations.add(role.getContainer().getId());
-        registerRoleInvalidation(role.getId());
+
+        invalidateRole(role.getId());
+        invalidationEvents.add(RoleRemovedEvent.create(role.getId(), role.getName(), role.getContainer().getId()));
+        roleRemovalInvalidations(role.getId(), role.getName(), role.getContainer().getId());
+
         return getDelegate().removeRole(realm, role);
     }
 
@@ -797,8 +832,11 @@ public class RealmCacheSession implements CacheRealmProvider {
 
     @Override
     public void moveGroup(RealmModel realm, GroupModel group, GroupModel toParent) {
-        registerGroupInvalidation(group.getId());
-        if (toParent != null) registerGroupInvalidation(toParent.getId());
+        invalidateGroup(group.getId(), realm.getId(), true);
+        if (toParent != null) invalidateGroup(group.getId(), realm.getId(), false); // Queries already invalidated
+        listInvalidations.add(realm.getId());
+
+        invalidationEvents.add(GroupMovedEvent.create(group, toParent, realm.getId()));
         getDelegate().moveGroup(realm, group, toParent);
     }
 
@@ -876,14 +914,15 @@ public class RealmCacheSession implements CacheRealmProvider {
 
     @Override
     public boolean removeGroup(RealmModel realm, GroupModel group) {
-        registerGroupInvalidation(group.getId());
+        invalidateGroup(group.getId(), realm.getId(), true);
         listInvalidations.add(realm.getId());
-        invalidations.add(getGroupsQueryCacheKey(realm.getId()));
-        if (group.getParentId() == null) {
-            invalidations.add(getTopGroupsQueryCacheKey(realm.getId()));
-        } else {
-            registerGroupInvalidation(group.getParentId());
+        cache.groupQueriesInvalidations(realm.getId(), invalidations);
+        if (group.getParentId() != null) {
+            invalidateGroup(group.getParentId(), realm.getId(), false); // Queries already invalidated
         }
+
+        invalidationEvents.add(GroupRemovedEvent.create(group, realm.getId()));
+
         return getDelegate().removeGroup(realm, group);
     }
 
@@ -893,11 +932,11 @@ public class RealmCacheSession implements CacheRealmProvider {
         return groupAdded(realm, group);
     }
 
-    public GroupModel groupAdded(RealmModel realm, GroupModel group) {
+    private GroupModel groupAdded(RealmModel realm, GroupModel group) {
         listInvalidations.add(realm.getId());
-        invalidations.add(getGroupsQueryCacheKey(realm.getId()));
-        invalidations.add(getTopGroupsQueryCacheKey(realm.getId()));
+        cache.groupQueriesInvalidations(realm.getId(), invalidations);
         invalidations.add(group.getId());
+        invalidationEvents.add(GroupAddedEvent.create(group.getId(), realm.getId()));
         return group;
     }
 
@@ -909,15 +948,32 @@ public class RealmCacheSession implements CacheRealmProvider {
 
     @Override
     public void addTopLevelGroup(RealmModel realm, GroupModel subGroup) {
-        invalidations.add(getTopGroupsQueryCacheKey(realm.getId()));
-        invalidations.add(subGroup.getId());
+        invalidateGroup(subGroup.getId(), realm.getId(), true);
         if (subGroup.getParentId() != null) {
-            registerGroupInvalidation(subGroup.getParentId());
+            invalidateGroup(subGroup.getParentId(), realm.getId(), false); // Queries already invalidated
         }
+
+        addGroupEventIfAbsent(GroupMovedEvent.create(subGroup, null, realm.getId()));
+
         getDelegate().addTopLevelGroup(realm, subGroup);
 
     }
 
+    private void addGroupEventIfAbsent(InvalidationEvent eventToAdd) {
+        String groupId = eventToAdd.getId();
+
+        // Check if we have existing event with bigger priority
+        boolean eventAlreadyExists = invalidationEvents.stream().filter((InvalidationEvent event) -> {
+
+            return (event.getId().equals(groupId)) && (event instanceof GroupAddedEvent || event instanceof GroupMovedEvent || event instanceof GroupRemovedEvent);
+
+        }).findFirst().isPresent();
+
+        if (!eventAlreadyExists) {
+            invalidationEvents.add(eventToAdd);
+        }
+    }
+
     @Override
     public ClientModel getClientById(String id, RealmModel realm) {
         CachedClient cached = cache.get(id, CachedClient.class);
@@ -948,7 +1004,7 @@ public class RealmCacheSession implements CacheRealmProvider {
 
     @Override
     public ClientModel getClientByClientId(String clientId, RealmModel realm) {
-        String cacheKey = getClientByClientIdCacheKey(clientId, realm);
+        String cacheKey = getClientByClientIdCacheKey(clientId, realm.getId());
         ClientListQuery query = cache.get(cacheKey, ClientListQuery.class);
         String id = null;
 
@@ -976,8 +1032,8 @@ public class RealmCacheSession implements CacheRealmProvider {
         return getClientById(id, realm);
     }
 
-    public String getClientByClientIdCacheKey(String clientId, RealmModel realm) {
-        return realm.getId() + ".client.query.by.clientId." + clientId;
+    static String getClientByClientIdCacheKey(String clientId, String realmId) {
+        return realmId + ".client.query.by.clientId." + clientId;
     }
 
     @Override
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RoleAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RoleAdapter.java
index a43aeb8..b6862f5 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RoleAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RoleAdapter.java
@@ -47,7 +47,7 @@ public class RoleAdapter implements RoleModel {
 
     protected void getDelegateForUpdate() {
         if (updated == null) {
-            cacheSession.registerRoleInvalidation(cached.getId());
+            cacheSession.registerRoleInvalidation(cached.getId(), cached.getName(), getContainerId());
             updated = cacheSession.getDelegate().getRoleById(cached.getId(), realm);
             if (updated == null) throw new IllegalStateException("Not found in database");
         }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheManager.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheManager.java
index ee8dc8b..e949314 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheManager.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheManager.java
@@ -18,40 +18,94 @@
 package org.keycloak.models.cache.infinispan;
 
 import org.infinispan.Cache;
-import org.infinispan.notifications.Listener;
 import org.jboss.logging.Logger;
+import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
 import org.keycloak.models.cache.infinispan.entities.Revisioned;
+import org.keycloak.models.cache.infinispan.events.UserCacheInvalidationEvent;
 import org.keycloak.models.cache.infinispan.stream.InRealmPredicate;
 
 import java.util.Map;
 import java.util.Set;
-import java.util.function.Predicate;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
  */
-@Listener
 public class UserCacheManager extends CacheManager {
 
-    protected static final Logger logger = Logger.getLogger(UserCacheManager.class);
+    private static final Logger logger = Logger.getLogger(UserCacheManager.class);
 
     protected volatile boolean enabled = true;
+
     public UserCacheManager(Cache<String, Revisioned> cache, Cache<String, Long> revisions) {
         super(cache, revisions);
     }
 
     @Override
+    protected Logger getLogger() {
+        return logger;
+    }
+
+    @Override
     public void clear() {
         cache.clear();
         revisions.clear();
     }
 
+
+    public void userUpdatedInvalidations(String userId, String username, String email, String realmId, Set<String> invalidations) {
+        invalidations.add(userId);
+        if (email != null) invalidations.add(UserCacheSession.getUserByEmailCacheKey(realmId, email));
+        invalidations.add(UserCacheSession.getUserByUsernameCacheKey(realmId, username));
+    }
+
+    // Fully invalidate user including consents and federatedIdentity links.
+    public void fullUserInvalidation(String userId, String username, String email, String realmId, boolean identityFederationEnabled, Map<String, String> federatedIdentities, Set<String> invalidations) {
+        userUpdatedInvalidations(userId, username, email, realmId, invalidations);
+
+        if (identityFederationEnabled) {
+            // Invalidate all keys for lookup this user by any identityProvider link
+            for (Map.Entry<String, String> socialLink : federatedIdentities.entrySet()) {
+                String fedIdentityCacheKey = UserCacheSession.getUserByFederatedIdentityCacheKey(realmId, socialLink.getKey(), socialLink.getValue());
+                invalidations.add(fedIdentityCacheKey);
+            }
+
+            // Invalidate federationLinks of user
+            invalidations.add(UserCacheSession.getFederatedIdentityLinksCacheKey(userId));
+        }
+
+        // Consents
+        invalidations.add(UserCacheSession.getConsentCacheKey(userId));
+    }
+
+    public void federatedIdentityLinkUpdatedInvalidation(String userId, Set<String> invalidations) {
+        invalidations.add(UserCacheSession.getFederatedIdentityLinksCacheKey(userId));
+    }
+
+    public void federatedIdentityLinkRemovedInvalidation(String userId, String realmId, String identityProviderId, String socialUserId, Set<String> invalidations) {
+        invalidations.add(UserCacheSession.getFederatedIdentityLinksCacheKey(userId));
+        if (identityProviderId != null) {
+            invalidations.add(UserCacheSession.getUserByFederatedIdentityCacheKey(realmId, identityProviderId, socialUserId));
+        }
+    }
+
+    public void consentInvalidation(String userId, Set<String> invalidations) {
+        invalidations.add(UserCacheSession.getConsentCacheKey(userId));
+    }
+
+
     @Override
-    protected Predicate<Map.Entry<String, Revisioned>> getInvalidationPredicate(Object object) {
-        return null;
+    protected void addInvalidationsFromEvent(InvalidationEvent event, Set<String> invalidations) {
+        if (event instanceof UserCacheInvalidationEvent) {
+            ((UserCacheInvalidationEvent) event).addInvalidations(this, invalidations);
+        }
     }
 
     public void invalidateRealmUsers(String realm, Set<String> invalidations) {
-        addInvalidations(InRealmPredicate.create().realm(realm), invalidations);
+        InRealmPredicate inRealmPredicate = getInRealmPredicate(realm);
+        addInvalidations(inRealmPredicate, invalidations);
+    }
+
+    private InRealmPredicate getInRealmPredicate(String realmId) {
+        return InRealmPredicate.create().realm(realmId);
     }
 }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java
index 5531de1..cb8c0a8 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java
@@ -19,6 +19,7 @@ package org.keycloak.models.cache.infinispan;
 
 import org.jboss.logging.Logger;
 import org.keycloak.cluster.ClusterProvider;
+import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
 import org.keycloak.common.constants.ServiceAccountConstants;
 import org.keycloak.common.util.Time;
 import org.keycloak.component.ComponentModel;
@@ -42,6 +43,12 @@ import org.keycloak.models.cache.infinispan.entities.CachedUser;
 import org.keycloak.models.cache.infinispan.entities.CachedUserConsent;
 import org.keycloak.models.cache.infinispan.entities.CachedUserConsents;
 import org.keycloak.models.cache.infinispan.entities.UserListQuery;
+import org.keycloak.models.cache.infinispan.events.UserCacheRealmInvalidationEvent;
+import org.keycloak.models.cache.infinispan.events.UserConsentsUpdatedEvent;
+import org.keycloak.models.cache.infinispan.events.UserFederationLinkRemovedEvent;
+import org.keycloak.models.cache.infinispan.events.UserFederationLinkUpdatedEvent;
+import org.keycloak.models.cache.infinispan.events.UserFullInvalidationEvent;
+import org.keycloak.models.cache.infinispan.events.UserUpdatedEvent;
 import org.keycloak.storage.StorageId;
 import org.keycloak.storage.UserStorageProvider;
 import org.keycloak.storage.UserStorageProviderModel;
@@ -72,6 +79,7 @@ public class UserCacheSession implements UserCache {
 
     protected Set<String> invalidations = new HashSet<>();
     protected Set<String> realmInvalidations = new HashSet<>();
+    protected Set<InvalidationEvent> invalidationEvents = new HashSet<>(); // Events to be sent across cluster
     protected Map<String, UserModel> managedUsers = new HashMap<>();
 
     public UserCacheSession(UserCacheManager cache, KeycloakSession session) {
@@ -85,7 +93,7 @@ public class UserCacheSession implements UserCache {
     public void clear() {
         cache.clear();
         ClusterProvider cluster = session.getProvider(ClusterProvider.class);
-        cluster.notify(InfinispanUserCacheProviderFactory.USER_CLEAR_CACHE_EVENTS, new ClearCacheEvent());
+        cluster.notify(InfinispanUserCacheProviderFactory.USER_CLEAR_CACHE_EVENTS, new ClearCacheEvent(), true);
     }
 
     public UserProvider getDelegate() {
@@ -97,10 +105,8 @@ public class UserCacheSession implements UserCache {
     }
 
     public void registerUserInvalidation(RealmModel realm,CachedUser user) {
-        invalidations.add(user.getId());
-        if (user.getEmail() != null) invalidations.add(getUserByEmailCacheKey(realm.getId(), user.getEmail()));
-        invalidations.add(getUserByUsernameCacheKey(realm.getId(), user.getUsername()));
-        if (realm.isIdentityFederationEnabled()) invalidations.add(getFederatedIdentityLinksCacheKey(user.getId()));
+        cache.userUpdatedInvalidations(user.getId(), user.getUsername(), user.getEmail(), user.getRealm(), invalidations);
+        invalidationEvents.add(UserUpdatedEvent.create(user.getId(), user.getUsername(), user.getEmail(), user.getRealm()));
     }
 
     @Override
@@ -108,16 +114,14 @@ public class UserCacheSession implements UserCache {
         if (user instanceof CachedUserModel) {
             ((CachedUserModel)user).invalidate();
         } else {
-            invalidations.add(user.getId());
-            if (user.getEmail() != null) invalidations.add(getUserByEmailCacheKey(realm.getId(), user.getEmail()));
-            invalidations.add(getUserByUsernameCacheKey(realm.getId(), user.getUsername()));
-            if (realm.isIdentityFederationEnabled()) invalidations.add(getFederatedIdentityLinksCacheKey(user.getId()));
+            cache.userUpdatedInvalidations(user.getId(), user.getUsername(), user.getEmail(), realm.getId(), invalidations);
+            invalidationEvents.add(UserUpdatedEvent.create(user.getId(), user.getUsername(), user.getEmail(), realm.getId()));
         }
     }
 
     @Override
     public void evict(RealmModel realm) {
-        realmInvalidations.add(realm.getId());
+        addRealmInvalidation(realm.getId());
     }
 
     protected void runInvalidations() {
@@ -127,6 +131,8 @@ public class UserCacheSession implements UserCache {
         for (String invalidation : invalidations) {
             cache.invalidateObject(invalidation);
         }
+
+        cache.sendInvalidationEvents(session, invalidationEvents);
     }
 
     private KeycloakTransaction getTransaction() {
@@ -201,19 +207,23 @@ public class UserCacheSession implements UserCache {
         return adapter;
     }
 
-    public String getUserByUsernameCacheKey(String realmId, String username) {
+    static String getUserByUsernameCacheKey(String realmId, String username) {
         return realmId + ".username." + username;
     }
 
-    public String getUserByEmailCacheKey(String realmId, String email) {
+    static String getUserByEmailCacheKey(String realmId, String email) {
         return realmId + ".email." + email;
     }
 
-    public String getUserByFederatedIdentityCacheKey(String realmId, FederatedIdentityModel socialLink) {
-        return realmId + ".idp." + socialLink.getIdentityProvider() + "." + socialLink.getUserId();
+    private static String getUserByFederatedIdentityCacheKey(String realmId, FederatedIdentityModel socialLink) {
+        return getUserByFederatedIdentityCacheKey(realmId, socialLink.getIdentityProvider(), socialLink.getUserId());
+    }
+
+    static String getUserByFederatedIdentityCacheKey(String realmId, String identityProvider, String socialUserId) {
+        return realmId + ".idp." + identityProvider + "." + socialUserId;
     }
 
-    public String getFederatedIdentityLinksCacheKey(String userId) {
+    static String getFederatedIdentityLinksCacheKey(String userId) {
         return userId + ".idplinks";
     }
 
@@ -655,27 +665,32 @@ public class UserCacheSession implements UserCache {
 
     @Override
     public void updateConsent(RealmModel realm, String userId, UserConsentModel consent) {
-        invalidations.add(getConsentCacheKey(userId));
+        invalidateConsent(userId);
         getDelegate().updateConsent(realm, userId, consent);
     }
 
     @Override
     public boolean revokeConsentForClient(RealmModel realm, String userId, String clientInternalId) {
-        invalidations.add(getConsentCacheKey(userId));
+        invalidateConsent(userId);
         return getDelegate().revokeConsentForClient(realm, userId, clientInternalId);
     }
 
-    public String getConsentCacheKey(String userId) {
+    static String getConsentCacheKey(String userId) {
         return userId + ".consents";
     }
 
 
     @Override
     public void addConsent(RealmModel realm, String userId, UserConsentModel consent) {
-        invalidations.add(getConsentCacheKey(userId));
+        invalidateConsent(userId);
         getDelegate().addConsent(realm, userId, consent);
     }
 
+    private void invalidateConsent(String userId) {
+        cache.consentInvalidation(userId, invalidations);
+        invalidationEvents.add(UserConsentsUpdatedEvent.create(userId));
+    }
+
     @Override
     public UserConsentModel getConsentByClient(RealmModel realm, String userId, String clientId) {
         logger.tracev("getConsentByClient: {0}", userId);
@@ -754,7 +769,7 @@ public class UserCacheSession implements UserCache {
     public UserModel addUser(RealmModel realm, String id, String username, boolean addDefaultRoles, boolean addDefaultRequiredActions) {
         UserModel user = getDelegate().addUser(realm, id, username, addDefaultRoles, addDefaultRoles);
         // just in case the transaction is rolled back you need to invalidate the user and all cache queries for that user
-        invalidateUser(realm, user);
+        fullyInvalidateUser(realm, user);
         managedUsers.put(user.getId(), user);
         return user;
     }
@@ -763,94 +778,89 @@ public class UserCacheSession implements UserCache {
     public UserModel addUser(RealmModel realm, String username) {
         UserModel user = getDelegate().addUser(realm, username);
         // just in case the transaction is rolled back you need to invalidate the user and all cache queries for that user
-        invalidateUser(realm, user);
+        fullyInvalidateUser(realm, user);
         managedUsers.put(user.getId(), user);
         return user;
     }
 
-    protected void invalidateUser(RealmModel realm, UserModel user) {
-        // just in case the transaction is rolled back you need to invalidate the user and all cache queries for that user
+    // just in case the transaction is rolled back you need to invalidate the user and all cache queries for that user
+    protected void fullyInvalidateUser(RealmModel realm, UserModel user) {
+        Set<FederatedIdentityModel> federatedIdentities = realm.isIdentityFederationEnabled() ? getFederatedIdentities(user, realm) : null;
 
-        if (realm.isIdentityFederationEnabled()) {
-            // Invalidate all keys for lookup this user by any identityProvider link
-            Set<FederatedIdentityModel> federatedIdentities = getFederatedIdentities(user, realm);
-            for (FederatedIdentityModel socialLink : federatedIdentities) {
-                String fedIdentityCacheKey = getUserByFederatedIdentityCacheKey(realm.getId(), socialLink);
-                invalidations.add(fedIdentityCacheKey);
-            }
+        UserFullInvalidationEvent event = UserFullInvalidationEvent.create(user.getId(), user.getUsername(), user.getEmail(), realm.getId(), realm.isIdentityFederationEnabled(), federatedIdentities);
 
-            // Invalidate federationLinks of user
-            invalidations.add(getFederatedIdentityLinksCacheKey(user.getId()));
-        }
-
-        invalidations.add(user.getId());
-        if (user.getEmail() != null) invalidations.add(getUserByEmailCacheKey(realm.getId(), user.getEmail()));
-        invalidations.add(getUserByUsernameCacheKey(realm.getId(), user.getUsername()));
+        cache.fullUserInvalidation(user.getId(), user.getUsername(), user.getEmail(), realm.getId(), realm.isIdentityFederationEnabled(), event.getFederatedIdentities(), invalidations);
+        invalidationEvents.add(event);
     }
 
     @Override
     public boolean removeUser(RealmModel realm, UserModel user) {
-        invalidateUser(realm, user);
+        fullyInvalidateUser(realm, user);
         return getDelegate().removeUser(realm, user);
     }
 
     @Override
     public void addFederatedIdentity(RealmModel realm, UserModel user, FederatedIdentityModel socialLink) {
-        invalidations.add(getFederatedIdentityLinksCacheKey(user.getId()));
+        invalidateFederationLink(user.getId());
         getDelegate().addFederatedIdentity(realm, user, socialLink);
     }
 
     @Override
     public void updateFederatedIdentity(RealmModel realm, UserModel federatedUser, FederatedIdentityModel federatedIdentityModel) {
-        invalidations.add(getFederatedIdentityLinksCacheKey(federatedUser.getId()));
+        invalidateFederationLink(federatedUser.getId());
         getDelegate().updateFederatedIdentity(realm, federatedUser, federatedIdentityModel);
     }
 
+    private void invalidateFederationLink(String userId) {
+        cache.federatedIdentityLinkUpdatedInvalidation(userId, invalidations);
+        invalidationEvents.add(UserFederationLinkUpdatedEvent.create(userId));
+    }
+
     @Override
     public boolean removeFederatedIdentity(RealmModel realm, UserModel user, String socialProvider) {
         // Needs to invalidate both directions
         FederatedIdentityModel socialLink = getFederatedIdentity(user, socialProvider, realm);
-        invalidations.add(getFederatedIdentityLinksCacheKey(user.getId()));
-        if (socialLink != null) {
-            invalidations.add(getUserByFederatedIdentityCacheKey(realm.getId(), socialLink));
-        }
+
+        UserFederationLinkRemovedEvent event = UserFederationLinkRemovedEvent.create(user.getId(), realm.getId(), socialLink);
+        cache.federatedIdentityLinkRemovedInvalidation(user.getId(), realm.getId(), event.getIdentityProviderId(), event.getSocialUserId(), invalidations);
+        invalidationEvents.add(event);
 
         return getDelegate().removeFederatedIdentity(realm, user, socialProvider);
     }
 
     @Override
     public void grantToAllUsers(RealmModel realm, RoleModel role) {
-        realmInvalidations.add(realm.getId()); // easier to just invalidate whole realm
+        addRealmInvalidation(realm.getId()); // easier to just invalidate whole realm
         getDelegate().grantToAllUsers(realm, role);
     }
 
     @Override
     public void preRemove(RealmModel realm) {
-        realmInvalidations.add(realm.getId());
+        addRealmInvalidation(realm.getId());
         getDelegate().preRemove(realm);
     }
 
     @Override
     public void preRemove(RealmModel realm, RoleModel role) {
-        realmInvalidations.add(realm.getId()); // easier to just invalidate whole realm
+        addRealmInvalidation(realm.getId()); // easier to just invalidate whole realm
         getDelegate().preRemove(realm, role);
     }
     @Override
     public void preRemove(RealmModel realm, GroupModel group) {
-        realmInvalidations.add(realm.getId()); // easier to just invalidate whole realm
+        addRealmInvalidation(realm.getId()); // easier to just invalidate whole realm
         getDelegate().preRemove(realm, group);
     }
 
 
     @Override
     public void preRemove(RealmModel realm, UserFederationProviderModel link) {
-        realmInvalidations.add(realm.getId()); // easier to just invalidate whole realm
+        addRealmInvalidation(realm.getId()); // easier to just invalidate whole realm
         getDelegate().preRemove(realm, link);
     }
 
     @Override
     public void preRemove(RealmModel realm, ClientModel client) {
-        realmInvalidations.add(realm.getId()); // easier to just invalidate whole realm
+        addRealmInvalidation(realm.getId()); // easier to just invalidate whole realm
         getDelegate().preRemove(realm, client);
     }
 
@@ -862,9 +872,14 @@ public class UserCacheSession implements UserCache {
     @Override
     public void preRemove(RealmModel realm, ComponentModel component) {
         if (!component.getProviderType().equals(UserStorageProvider.class.getName())) return;
-        realmInvalidations.add(realm.getId()); // easier to just invalidate whole realm
+        addRealmInvalidation(realm.getId()); // easier to just invalidate whole realm
         getDelegate().preRemove(realm, component);
 
     }
 
+    private void addRealmInvalidation(String realmId) {
+        realmInvalidations.add(realmId);
+        invalidationEvents.add(UserCacheRealmInvalidationEvent.create(realmId));
+    }
+
 }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
index 44419cd..c21f787 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
@@ -431,8 +431,8 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
         }
     }
 
-    @Override
-    public void onUserRemoved(RealmModel realm, UserModel user) {
+
+    protected void onUserRemoved(RealmModel realm, UserModel user) {
         removeUserSessions(realm, user, true);
         removeUserSessions(realm, user, false);
 
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java
index 343f2f0..663a4b2 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java
@@ -24,6 +24,7 @@ import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
 import org.keycloak.models.KeycloakSessionTask;
+import org.keycloak.models.UserModel;
 import org.keycloak.models.UserSessionProvider;
 import org.keycloak.models.UserSessionProviderFactory;
 import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
@@ -45,7 +46,7 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
     private Config.Scope config;
 
     @Override
-    public UserSessionProvider create(KeycloakSession session) {
+    public InfinispanUserSessionProvider create(KeycloakSession session) {
         InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
         Cache<String, SessionEntity> cache = connections.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME);
         Cache<String, SessionEntity> offlineSessionsCache = connections.getCache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME);
@@ -73,6 +74,11 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
             public void onEvent(ProviderEvent event) {
                 if (event instanceof PostMigrationEvent) {
                     loadPersistentSessions(factory, maxErrors, sessionsPerSegment);
+                } else if (event instanceof UserModel.UserRemovedEvent) {
+                    UserModel.UserRemovedEvent userRemovedEvent = (UserModel.UserRemovedEvent) event;
+
+                    InfinispanUserSessionProvider provider = (InfinispanUserSessionProvider) userRemovedEvent.getKeycloakSession().getProvider(UserSessionProvider.class, getId());
+                    provider.onUserRemoved(userRemovedEvent.getRealm(), userRemovedEvent.getUser());
                 }
             }
         });
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java
index 1485da8..c332eea 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java
@@ -92,13 +92,13 @@ public class InfinispanUserSessionInitializer {
 
 
     private boolean isFinished() {
-        InitializerState state = (InitializerState) workCache.get(stateKey);
+        InitializerState state = getStateFromCache();
         return state != null && state.isFinished();
     }
 
 
     private InitializerState getOrCreateInitializerState() {
-        InitializerState state = (InitializerState) workCache.get(stateKey);
+        InitializerState state = getStateFromCache();
         if (state == null) {
             final int[] count = new int[1];
 
@@ -128,6 +128,12 @@ public class InfinispanUserSessionInitializer {
 
     }
 
+    private InitializerState getStateFromCache() {
+        // TODO: We ignore cacheStore for now, so that in Cross-DC scenario (with RemoteStore enabled) is the remoteStore ignored. This means that every DC needs to load offline sessions separately.
+        return (InitializerState) workCache.getAdvancedCache()
+                .withFlags(Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD)
+                .get(stateKey);
+    }
 
     private void saveStateToCache(final InitializerState state) {
 
@@ -138,8 +144,9 @@ public class InfinispanUserSessionInitializer {
             public void run() {
 
                 // Save this synchronously to ensure all nodes read correct state
+                // TODO: We ignore cacheStore for now, so that in Cross-DC scenario (with RemoteStore enabled) is the remoteStore ignored. This means that every DC needs to load offline sessions separately.
                 InfinispanUserSessionInitializer.this.workCache.getAdvancedCache().
-                        withFlags(Flag.IGNORE_RETURN_VALUES, Flag.FORCE_SYNCHRONOUS)
+                        withFlags(Flag.IGNORE_RETURN_VALUES, Flag.FORCE_SYNCHRONOUS, Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD)
                         .put(stateKey, state);
             }
 
diff --git a/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheTest.java b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheTest.java
new file mode 100644
index 0000000..e7c1337
--- /dev/null
+++ b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheTest.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.cluster.infinispan;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.infinispan.Cache;
+import org.infinispan.client.hotrod.Flag;
+import org.infinispan.client.hotrod.RemoteCache;
+import org.infinispan.client.hotrod.annotation.ClientCacheEntryCreated;
+import org.infinispan.client.hotrod.annotation.ClientCacheEntryModified;
+import org.infinispan.client.hotrod.annotation.ClientListener;
+import org.infinispan.client.hotrod.event.ClientCacheEntryCreatedEvent;
+import org.infinispan.client.hotrod.event.ClientCacheEntryModifiedEvent;
+import org.infinispan.configuration.cache.Configuration;
+import org.infinispan.configuration.cache.ConfigurationBuilder;
+import org.infinispan.configuration.global.GlobalConfigurationBuilder;
+import org.infinispan.manager.DefaultCacheManager;
+import org.infinispan.manager.EmbeddedCacheManager;
+import org.infinispan.persistence.manager.PersistenceManager;
+import org.infinispan.persistence.remote.RemoteStore;
+import org.infinispan.persistence.remote.configuration.ExhaustedAction;
+import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder;
+import org.junit.Ignore;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+
+/**
+ * Test concurrency for remoteStore (backed by HotRod RemoteCaches) against external JDG
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@Ignore
+public class ConcurrencyJDGRemoteCacheTest {
+
+    private static Map<String, EntryInfo> state = new HashMap<>();
+
+    public static void main(String[] args) throws Exception {
+        // Init map somehow
+        for (int i=0 ; i<100 ; i++) {
+            String key = "key-" + i;
+            state.put(key, new EntryInfo());
+        }
+
+        // Create caches, listeners and finally worker threads
+        Worker worker1 = createWorker(1);
+        Worker worker2 = createWorker(2);
+
+        // Start and join workers
+        worker1.start();
+        worker2.start();
+
+        worker1.join();
+        worker2.join();
+
+        // Output
+        for (Map.Entry<String, EntryInfo> entry : state.entrySet()) {
+            System.out.println(entry.getKey() + ":::" + entry.getValue());
+            worker1.cache.remove(entry.getKey());
+        }
+
+        // Finish JVM
+        worker1.cache.getCacheManager().stop();
+        worker2.cache.getCacheManager().stop();
+    }
+
+    private static Worker createWorker(int threadId) {
+        EmbeddedCacheManager manager = createManager(threadId);
+        Cache<String, Integer> cache = manager.getCache(InfinispanConnectionProvider.WORK_CACHE_NAME);
+
+        System.out.println("Retrieved cache: " + threadId);
+
+        RemoteStore remoteStore = cache.getAdvancedCache().getComponentRegistry().getComponent(PersistenceManager.class).getStores(RemoteStore.class).iterator().next();
+        HotRodListener listener = new HotRodListener();
+        remoteStore.getRemoteCache().addClientListener(listener);
+
+        return new Worker(cache, threadId);
+    }
+
+    private static EmbeddedCacheManager createManager(int threadId) {
+        System.setProperty("java.net.preferIPv4Stack", "true");
+        System.setProperty("jgroups.tcp.port", "53715");
+        GlobalConfigurationBuilder gcb = new GlobalConfigurationBuilder();
+
+        boolean clustered = false;
+        boolean async = false;
+        boolean allowDuplicateJMXDomains = true;
+
+        if (clustered) {
+            gcb = gcb.clusteredDefault();
+            gcb.transport().clusterName("test-clustering");
+        }
+
+        gcb.globalJmxStatistics().allowDuplicateDomains(allowDuplicateJMXDomains);
+
+        EmbeddedCacheManager cacheManager = new DefaultCacheManager(gcb.build());
+
+        Configuration invalidationCacheConfiguration = getCacheBackedByRemoteStore(threadId);
+
+        cacheManager.defineConfiguration(InfinispanConnectionProvider.WORK_CACHE_NAME, invalidationCacheConfiguration);
+        return cacheManager;
+
+    }
+
+    private static Configuration getCacheBackedByRemoteStore(int threadId) {
+        ConfigurationBuilder cacheConfigBuilder = new ConfigurationBuilder();
+
+        // int port = threadId==1 ? 11222 : 11322;
+        int port = 11222;
+
+        return cacheConfigBuilder.persistence().addStore(RemoteStoreConfigurationBuilder.class)
+                .fetchPersistentState(false)
+                .ignoreModifications(false)
+                .purgeOnStartup(false)
+                .preload(false)
+                .shared(true)
+                .remoteCacheName(InfinispanConnectionProvider.WORK_CACHE_NAME)
+                .rawValues(true)
+                .forceReturnValues(false)
+                .addServer()
+                    .host("localhost")
+                    .port(port)
+                .connectionPool()
+                    .maxActive(20)
+                    .exhaustedAction(ExhaustedAction.CREATE_NEW)
+                .async()
+                .   enabled(false).build();
+    }
+
+
+    @ClientListener
+    public static class HotRodListener {
+
+        //private AtomicInteger listenerCount = new AtomicInteger(0);
+
+        @ClientCacheEntryCreated
+        public void created(ClientCacheEntryCreatedEvent event) {
+            String cacheKey = (String) event.getKey();
+            state.get(cacheKey).successfulListenerWrites.incrementAndGet();
+        }
+
+        @ClientCacheEntryModified
+        public void updated(ClientCacheEntryModifiedEvent event) {
+            String cacheKey = (String) event.getKey();
+            state.get(cacheKey).successfulListenerWrites.incrementAndGet();
+        }
+
+    }
+
+
+    private static class Worker extends Thread {
+
+        private final Cache<String, Integer> cache;
+
+        private final int myThreadId;
+
+        private Worker(Cache<String, Integer> cache, int myThreadId) {
+            this.cache = cache;
+            this.myThreadId = myThreadId;
+        }
+
+        @Override
+        public void run() {
+            for (Map.Entry<String, EntryInfo> entry : state.entrySet()) {
+                String cacheKey = entry.getKey();
+                EntryInfo wrapper = state.get(cacheKey);
+
+                int val = getClusterStartupTime(this.cache, cacheKey, wrapper);
+                if (myThreadId == 1) {
+                    wrapper.th1.set(val);
+                } else {
+                    wrapper.th2.set(val);
+                }
+
+            }
+
+            System.out.println("Worker finished: " + myThreadId);
+        }
+
+    }
+
+    public static int getClusterStartupTime(Cache<String, Integer> cache, String cacheKey, EntryInfo wrapper) {
+        int startupTime = new Random().nextInt(1024);
+
+        // Concurrency doesn't work correctly with this
+        //Integer existingClusterStartTime = (Integer) cache.putIfAbsent(cacheKey, startupTime);
+
+        // Concurrency works fine with this
+        RemoteCache remoteCache = cache.getAdvancedCache().getComponentRegistry().getComponent(PersistenceManager.class).getStores(RemoteStore.class).iterator().next().getRemoteCache();
+        Integer existingClusterStartTime = (Integer) remoteCache.withFlags(Flag.FORCE_RETURN_VALUE).putIfAbsent(cacheKey, startupTime);
+
+        if (existingClusterStartTime == null) {
+            wrapper.successfulInitializations.incrementAndGet();
+            return startupTime;
+        } else {
+            return existingClusterStartTime;
+        }
+    }
+
+    private static class EntryInfo {
+        AtomicInteger successfulInitializations = new AtomicInteger(0);
+        AtomicInteger successfulListenerWrites = new AtomicInteger(0);
+        AtomicInteger th1 = new AtomicInteger();
+        AtomicInteger th2 = new AtomicInteger();
+
+        @Override
+        public String toString() {
+            return String.format("Inits: %d, listeners: %d, th1: %d, th2: %d", successfulInitializations.get(), successfulListenerWrites.get(), th1.get(), th2.get());
+        }
+    }
+
+
+
+}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java
index f5d2666..55e4108 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java
@@ -146,7 +146,8 @@ public class JpaRealmProvider implements RealmProvider {
         query.setParameter("realm", realm.getId());
         List<String> clients = query.getResultList();
         for (String client : clients) {
-            session.realms().removeClient(client, adapter);
+            // No need to go through cache. Clients were already invalidated
+            removeClient(client, adapter);
         }
 
         for (ClientTemplateEntity a : new LinkedList<>(realm.getClientTemplates())) {
@@ -154,7 +155,8 @@ public class JpaRealmProvider implements RealmProvider {
         }
 
         for (RoleModel role : adapter.getRoles()) {
-            session.realms().removeRole(adapter, role);
+            // No need to go through cache. Roles were already invalidated
+            removeRole(adapter, role);
         }
 
 
@@ -486,7 +488,8 @@ public class JpaRealmProvider implements RealmProvider {
         session.users().preRemove(realm, client);
 
         for (RoleModel role : client.getRoles()) {
-            client.removeRole(role);
+            // No need to go through cache. Roles were already invalidated
+            removeRole(realm, role);
         }
 
         ClientEntity clientEntity = ((ClientAdapter)client).getEntity();
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
index b0ea73a..9633f84 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
@@ -124,17 +124,6 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
         UserEntity userEntity = em.find(UserEntity.class, user.getId());
         if (userEntity == null) return false;
         removeUser(userEntity);
-        session.getKeycloakSessionFactory().publish(new UserModel.UserRemovedEvent() {
-            @Override
-            public UserModel getUser() {
-                return user;
-            }
-
-            @Override
-            public KeycloakSession getKeycloakSession() {
-                return session;
-            }
-        });
         return true;
     }
 
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index 97aa4bd..f5b9d7d 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
@@ -953,13 +953,6 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
     }
 
     @Override
-    public boolean removeRoleById(String id) {
-        RoleModel role = getRoleById(id);
-        if (role == null) return false;
-        return role.getContainer().removeRole(role);
-    }
-
-    @Override
     public PasswordPolicy getPasswordPolicy() {
         if (passwordPolicy == null) {
             passwordPolicy = PasswordPolicy.parse(session, realm.getPasswordPolicy());
@@ -1933,12 +1926,6 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
     }
 
     @Override
-    public void addTopLevelGroup(GroupModel subGroup) {
-        session.realms().addTopLevelGroup(this, subGroup);
-
-    }
-
-    @Override
     public void moveGroup(GroupModel group, GroupModel toParent) {
         session.realms().moveGroup(this, group, toParent);
     }
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProviderFactory.java b/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProviderFactory.java
index 254412b..35265af 100644
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProviderFactory.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProviderFactory.java
@@ -45,11 +45,6 @@ public class JpaUserSessionPersisterProviderFactory implements UserSessionPersis
     }
 
     @Override
-    public void postInit(KeycloakSessionFactory factory) {
-
-    }
-
-    @Override
     public void close() {
 
     }
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java
index 7b46c0c..6ef597e 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java
@@ -41,6 +41,7 @@ import org.keycloak.models.RoleModel;
 import org.keycloak.models.UserConsentModel;
 import org.keycloak.models.UserCredentialModel;
 import org.keycloak.models.UserFederationProviderModel;
+import org.keycloak.models.UserManager;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.UserProvider;
 import org.keycloak.models.cache.CachedUserModel;
@@ -50,6 +51,8 @@ import org.keycloak.models.mongo.keycloak.entities.MongoUserConsentEntity;
 import org.keycloak.models.mongo.keycloak.entities.MongoUserEntity;
 import org.keycloak.models.mongo.keycloak.entities.UserConsentEntity;
 import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.models.utils.UserModelDelegate;
+import org.keycloak.storage.UserStorageProvider;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -630,7 +633,19 @@ public class MongoUserProvider implements UserProvider, UserCredentialStore {
 
     @Override
     public void preRemove(RealmModel realm, ComponentModel component) {
+        if (!component.getProviderType().equals(UserStorageProvider.class.getName())) return;
+        DBObject query = new QueryBuilder()
+                .and("federationLink").is(component.getId())
+                .get();
+
+        List<MongoUserEntity> mongoUsers = getMongoStore().loadEntities(MongoUserEntity.class, query, invocationContext);
+        UserManager userManager = new UserManager(session);
 
+        for (MongoUserEntity userEntity : mongoUsers) {
+            // Doing this way to ensure UserRemovedEvent triggered with proper callbacks.
+            UserAdapter user = new UserAdapter(session, realm, userEntity, invocationContext);
+            userManager.removeUser(realm, user, this);
+        }
     }
 
     @Override
@@ -661,16 +676,18 @@ public class MongoUserProvider implements UserProvider, UserCredentialStore {
     }
 
     public MongoUserEntity getMongoUserEntity(UserModel user) {
-        UserAdapter adapter = null;
-        if (user instanceof CachedUserModel) {
-            adapter = (UserAdapter)((CachedUserModel)user).getDelegateForUpdate();
-        } else if (user instanceof UserAdapter ){
-            adapter = (UserAdapter)user;
+        if (user instanceof UserAdapter) {
+            UserAdapter adapter = (UserAdapter)user;
+            return adapter.getMongoEntity();
+        } else if (user instanceof CachedUserModel) {
+            UserModel delegate = ((CachedUserModel)user).getDelegateForUpdate();
+            return getMongoUserEntity(delegate);
+        } else if (user instanceof UserModelDelegate){
+            UserModel delegate = ((UserModelDelegate) user).getDelegate();
+            return getMongoUserEntity(delegate);
         } else {
             return getMongoStore().loadEntity(MongoUserEntity.class, user.getId(), invocationContext);
-
         }
-        return adapter.getMongoEntity();
     }
 
     @Override
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserSessionPersisterProviderFactory.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserSessionPersisterProviderFactory.java
index b4028a2..083a0e6 100644
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserSessionPersisterProviderFactory.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserSessionPersisterProviderFactory.java
@@ -43,11 +43,6 @@ public class MongoUserSessionPersisterProviderFactory implements UserSessionPers
     }
 
     @Override
-    public void postInit(KeycloakSessionFactory factory) {
-
-    }
-
-    @Override
     public void close() {
 
     }
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
index a79c478..119c7df 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
@@ -23,7 +23,6 @@ import org.keycloak.common.enums.SslRequired;
 import org.keycloak.common.util.MultivaluedHashMap;
 import org.keycloak.component.ComponentModel;
 import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
-import org.keycloak.jose.jwk.JWKBuilder;
 import org.keycloak.models.AuthenticationExecutionModel;
 import org.keycloak.models.AuthenticationFlowModel;
 import org.keycloak.models.AuthenticatorConfigModel;
@@ -62,10 +61,6 @@ import org.keycloak.models.mongo.keycloak.entities.UserFederationProviderEntity;
 import org.keycloak.models.utils.ComponentUtil;
 import org.keycloak.models.utils.KeycloakModelUtils;
 
-import java.security.Key;
-import java.security.PrivateKey;
-import java.security.PublicKey;
-import java.security.cert.X509Certificate;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -516,13 +511,6 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
     }
 
     @Override
-    public boolean removeRoleById(String id) {
-        RoleModel role = getRoleById(id);
-        if (role == null) return false;
-        return removeRole(role);
-    }
-
-    @Override
     public Set<RoleModel> getRoles() {
         DBObject query = new QueryBuilder()
                 .and("realmId").is(getId())
@@ -555,12 +543,6 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
     }
 
     @Override
-    public void addTopLevelGroup(GroupModel subGroup) {
-        session.realms().addTopLevelGroup(this, subGroup);
-
-    }
-
-    @Override
     public void moveGroup(GroupModel group, GroupModel toParent) {
         session.realms().moveGroup(this, group, toParent);
     }
@@ -2006,28 +1988,39 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
     @Override
     public void removeComponent(ComponentModel component) {
         Iterator<ComponentEntity> it = realm.getComponentEntities().iterator();
+        ComponentEntity found = null;
         while(it.hasNext()) {
-            if (it.next().getId().equals(component.getId())) {
-                session.users().preRemove(this, component);
-                removeComponents(component.getId());
-                it.remove();
+            ComponentEntity next = it.next();
+            if (next.getId().equals(component.getId())) {
+                found = next;
                 break;
             }
         }
-        updateRealm();
 
+        if (found != null) {
+            session.users().preRemove(this, component);
+            removeComponents(component.getId());
+            realm.getComponentEntities().remove(found);
+            updateRealm();
+        }
     }
 
     @Override
     public void removeComponents(String parentId) {
         Iterator<ComponentEntity> it = realm.getComponentEntities().iterator();
+        Set<ComponentEntity> toRemove = new HashSet<>();
         while(it.hasNext()) {
             ComponentEntity next = it.next();
             if (next.getParentId().equals(parentId)) {
-                session.users().preRemove(this, entityToModel(next));
-                it.remove();
+                toRemove.add(next);
             }
         }
+
+        for (ComponentEntity toRem : toRemove) {
+            session.users().preRemove(this, entityToModel(toRem));
+            realm.getComponentEntities().remove(toRem);
+        }
+
         updateRealm();
 
     }
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 9d5ad7c..e5440cc 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
@@ -261,6 +261,7 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
 
     @Override
     public boolean isMemberOf(GroupModel group) {
+        if (user.getGroupIds() == null) return false;
         if (user.getGroupIds().contains(group.getId())) return true;
         Set<GroupModel> groups = getGroups();
         return RoleUtils.isMember(groups, group);

pom.xml 13(+6 -7)

diff --git a/pom.xml b/pom.xml
index 7918c6d..afd4212 100755
--- a/pom.xml
+++ b/pom.xml
@@ -80,7 +80,7 @@
 
         <!-- Authorization Drools Policy Provider -->
         <version.org.drools>6.4.0.Final</version.org.drools>
-        <version.jboss-integration-platform>6.0.6.Final</version.jboss-integration-platform>
+        <version.jboss-integration-platform>6.0.10.Final</version.jboss-integration-platform>
 
         <!-- Others -->
         <apacheds.version>2.0.0-M21</apacheds.version>
@@ -98,7 +98,6 @@
         <servlet.api.30.version>1.0.2.Final</servlet.api.30.version>
         <twitter4j.version>4.0.4</twitter4j.version>
         <jna.version>4.1.0</jna.version>
-        <jnr.version>0.14</jnr.version>
 
         <!-- Test -->
         <greenmail.version>1.3.1b</greenmail.version>
@@ -635,6 +634,11 @@
                 <version>${infinispan.version}</version>
             </dependency>
             <dependency>
+                <groupId>org.infinispan</groupId>
+                <artifactId>infinispan-cachestore-remote</artifactId>
+                <version>${infinispan.version}</version>
+            </dependency>
+            <dependency>
                 <groupId>org.liquibase</groupId>
                 <artifactId>liquibase-core</artifactId>
                 <version>${liquibase.version}</version>
@@ -703,11 +707,6 @@
                 <version>${jna.version}</version>
             </dependency>
             <dependency>
-                <groupId>com.github.jnr</groupId>
-                <artifactId>jnr-unixsocket</artifactId>
-                <version>${jnr.version}</version>
-            </dependency>
-            <dependency>
                 <groupId>org.keycloak</groupId>
                 <artifactId>keycloak-ldap-federation</artifactId>
                 <version>${project.version}</version>

README.md 2(+1 -1)

diff --git a/README.md b/README.md
index 983d44c..a4454a0 100755
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@ For more information about Keycloak visit [Keycloak homepage](http://keycloak.or
 Building
 --------
 
-Ensure you have JDK 8 (or newer), Maven 3.2.1 (or newer) and Git installed
+Ensure you have JDK 8 (or newer), Maven 3.1.1 (or newer) and Git installed
 
     java -version
     mvn -version
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/core/util/XMLEncryptionUtil.java b/saml-core/src/main/java/org/keycloak/saml/processing/core/util/XMLEncryptionUtil.java
index 245cff9..f81ea03 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/core/util/XMLEncryptionUtil.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/core/util/XMLEncryptionUtil.java
@@ -20,6 +20,7 @@ import org.apache.xml.security.encryption.EncryptedData;
 import org.apache.xml.security.encryption.EncryptedKey;
 import org.apache.xml.security.encryption.XMLCipher;
 import org.apache.xml.security.encryption.XMLEncryptionException;
+import org.apache.xml.security.utils.EncryptionConstants;
 
 import org.keycloak.saml.common.PicketLinkLogger;
 import org.keycloak.saml.common.PicketLinkLoggerFactory;
@@ -38,8 +39,9 @@ import javax.xml.namespace.QName;
 import java.security.Key;
 import java.security.PrivateKey;
 import java.security.PublicKey;
-import java.util.HashMap;
 import java.util.Objects;
+import javax.xml.XMLConstants;
+import javax.xml.crypto.dsig.XMLSignature;
 
 /**
  * Utility for XML Encryption <b>Note: </b> This utility is currently using Apache XML Security library API. JSR-106 is
@@ -58,77 +60,12 @@ public class XMLEncryptionUtil {
         org.apache.xml.security.Init.init();
     }
 
-    public static final String CIPHER_DATA_LOCALNAME = "CipherData";
-
-    public static final String ENCRYPTED_KEY_LOCALNAME = "EncryptedKey";
-
     public static final String DS_KEY_INFO = "ds:KeyInfo";
 
-    public static final String XMLNS = "http://www.w3.org/2000/xmlns/";
-
-    public static final String XMLSIG_NS = "http://www.w3.org/2000/09/xmldsig#";
-
-    public static final String XMLENC_NS = "http://www.w3.org/2001/04/xmlenc#";
-
-    private static HashMap<String, EncryptionAlgorithm> algorithms = new HashMap<String, EncryptionAlgorithm>(4);
-
     private static final String RSA_ENCRYPTION_SCHEME = Objects.equals(System.getProperty("keycloak.saml.key_trans.rsa_v1.5"), "true")
       ? XMLCipher.RSA_v1dot5
       : XMLCipher.RSA_OAEP;
 
-    private static class EncryptionAlgorithm {
-
-        EncryptionAlgorithm(String jceName, String xmlSecName, int size) {
-            this.jceName = jceName;
-            this.xmlSecName = xmlSecName;
-            this.size = size;
-        }
-
-        @SuppressWarnings("unused")
-        public String jceName;
-
-        public String xmlSecName;
-
-        public int size;
-    }
-
-    static {
-        algorithms.put("aes-128", new EncryptionAlgorithm("AES", XMLCipher.AES_128, 128));
-        algorithms.put("aes-192", new EncryptionAlgorithm("AES", XMLCipher.AES_192, 192));
-        algorithms.put("aes-256", new EncryptionAlgorithm("AES", XMLCipher.AES_256, 256));
-        algorithms.put("aes", new EncryptionAlgorithm("AES", XMLCipher.AES_256, 256));
-
-        algorithms.put("tripledes", new EncryptionAlgorithm("TripleDes", XMLCipher.TRIPLEDES, 168));
-    }
-
-    /**
-     * Given the JCE algorithm, get the XML Encryption URL
-     *
-     * @param certAlgo
-     *
-     * @return
-     */
-    public static String getEncryptionURL(String certAlgo) {
-        EncryptionAlgorithm ea = algorithms.get(certAlgo);
-        if (ea == null)
-            throw logger.encryptUnknownAlgoError(certAlgo);
-        return ea.xmlSecName;
-    }
-
-    /**
-     * Given the JCE algorithm, get the XML Encryption KeySize
-     *
-     * @param certAlgo
-     *
-     * @return
-     */
-    public static int getEncryptionKeySize(String certAlgo) {
-        EncryptionAlgorithm ea = algorithms.get(certAlgo);
-        if (ea == null)
-            throw logger.encryptUnknownAlgoError(certAlgo);
-        return ea.size;
-    }
-
     /**
      * <p>
      * Encrypt the Key to be transported
@@ -151,7 +88,7 @@ public class XMLEncryptionUtil {
      */
     public static EncryptedKey encryptKey(Document document, SecretKey keyToBeEncrypted, PublicKey keyUsedToEncryptSecretKey,
                                           int keySize) throws ProcessingException {
-        XMLCipher keyCipher = null;
+        XMLCipher keyCipher;
         String pubKeyAlg = keyUsedToEncryptSecretKey.getAlgorithm();
 
         try {
@@ -170,14 +107,13 @@ public class XMLEncryptionUtil {
      * data
      *
      * @param elementQName QName of the element that we like to encrypt
+     * @param document
      * @param publicKey
      * @param secretKey
      * @param keySize
      * @param wrappingElementQName A QName of an element that will wrap the encrypted element
      * @param addEncryptedKeyInKeyInfo Need for the EncryptedKey to be placed in ds:KeyInfo
      *
-     * @return
-     *
      * @throws ProcessingException
      */
     public static void encryptElement(QName elementQName, Document document, PublicKey publicKey, SecretKey secretKey,
@@ -187,7 +123,7 @@ public class XMLEncryptionUtil {
         if (document == null)
             throw logger.nullArgumentError("document");
         String wrappingElementPrefix = wrappingElementQName.getPrefix();
-        if (wrappingElementPrefix == null || wrappingElementPrefix == "")
+        if (wrappingElementPrefix == null || "".equals(wrappingElementPrefix))
             throw logger.wrongTypeError("Wrapping element prefix invalid");
 
         Element documentElement = DocumentUtil.getElement(document, elementQName);
@@ -217,18 +153,22 @@ public class XMLEncryptionUtil {
         // The EncryptedKey element is added
         Element encryptedKeyElement = cipher.martial(document, encryptedKey);
 
-        String wrappingElementName = wrappingElementPrefix + ":" + wrappingElementQName.getLocalPart();
+        final String wrappingElementName;
 
+        if (StringUtil.isNullOrEmpty(wrappingElementPrefix)) {
+            wrappingElementName = wrappingElementQName.getLocalPart();
+        } else {
+            wrappingElementName = wrappingElementPrefix + ":" + wrappingElementQName.getLocalPart();
+        }
         // Create the wrapping element and set its attribute NS
         Element wrappingElement = encryptedDoc.createElementNS(wrappingElementQName.getNamespaceURI(), wrappingElementName);
 
-        if (StringUtil.isNullOrEmpty(wrappingElementPrefix)) {
-            wrappingElementName = wrappingElementQName.getLocalPart();
+        if (! StringUtil.isNullOrEmpty(wrappingElementPrefix)) {
+            wrappingElement.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, "xmlns:" + wrappingElementPrefix, wrappingElementQName.getNamespaceURI());
         }
-        wrappingElement.setAttributeNS(XMLNS, "xmlns:" + wrappingElementPrefix, wrappingElementQName.getNamespaceURI());
 
         // Get Hold of the Cipher Data
-        NodeList cipherElements = encryptedDoc.getElementsByTagNameNS(XMLENC_NS, "EncryptedData");
+        NodeList cipherElements = encryptedDoc.getElementsByTagNameNS(EncryptionConstants.EncryptionSpecNS, EncryptionConstants._TAG_ENCRYPTEDDATA);
         if (cipherElements == null || cipherElements.getLength() == 0)
             throw logger.domMissingElementError("xenc:EncryptedData");
         Element encryptedDataElement = (Element) cipherElements.item(0);
@@ -240,12 +180,12 @@ public class XMLEncryptionUtil {
 
         if (addEncryptedKeyInKeyInfo) {
             // Outer ds:KeyInfo Element to hold the EncryptionKey
-            Element sigElement = encryptedDoc.createElementNS(XMLSIG_NS, DS_KEY_INFO);
-            sigElement.setAttributeNS(XMLNS, "xmlns:ds", XMLSIG_NS);
+            Element sigElement = encryptedDoc.createElementNS(XMLSignature.XMLNS, DS_KEY_INFO);
+            sigElement.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, "xmlns:ds", XMLSignature.XMLNS);
             sigElement.appendChild(encryptedKeyElement);
 
             // Insert the Encrypted key before the CipherData element
-            NodeList nodeList = encryptedDoc.getElementsByTagNameNS(XMLENC_NS, CIPHER_DATA_LOCALNAME);
+            NodeList nodeList = encryptedDoc.getElementsByTagNameNS(EncryptionConstants.EncryptionSpecNS, EncryptionConstants._TAG_CIPHERDATA);
             if (nodeList == null || nodeList.getLength() == 0)
                 throw logger.domMissingElementError("xenc:CipherData");
             Element cipherDataElement = (Element) nodeList.item(0);
@@ -328,12 +268,12 @@ public class XMLEncryptionUtil {
         Element encryptedKeyElement = cipher.martial(document, encryptedKey);
 
         // Outer ds:KeyInfo Element to hold the EncryptionKey
-        Element sigElement = encryptedDoc.createElementNS(XMLSIG_NS, DS_KEY_INFO);
-        sigElement.setAttributeNS(XMLNS, "xmlns:ds", XMLSIG_NS);
+        Element sigElement = encryptedDoc.createElementNS(XMLSignature.XMLNS, DS_KEY_INFO);
+        sigElement.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, "xmlns:ds", XMLSignature.XMLNS);
         sigElement.appendChild(encryptedKeyElement);
 
         // Insert the Encrypted key before the CipherData element
-        NodeList nodeList = encryptedDoc.getElementsByTagNameNS(XMLENC_NS, CIPHER_DATA_LOCALNAME);
+        NodeList nodeList = encryptedDoc.getElementsByTagNameNS(EncryptionConstants.EncryptionSpecNS, EncryptionConstants._TAG_CIPHERDATA);
         if (nodeList == null || nodeList.getLength() == 0)
             throw logger.domMissingElementError("xenc:CipherData");
         Element cipherDataElement = (Element) nodeList.item(0);
@@ -342,7 +282,7 @@ public class XMLEncryptionUtil {
     }
 
     /**
-     * Encrypt the root document element inside a Document. <b>NOTE:</> The document root element will be replaced by
+     * Encrypt the root document element inside a Document. <b>NOTE:</b> The document root element will be replaced by
      * the
      * wrapping element.
      *
@@ -361,7 +301,7 @@ public class XMLEncryptionUtil {
     public static Element encryptElementInDocument(Document document, PublicKey publicKey, SecretKey secretKey, int keySize,
                                                    QName wrappingElementQName, boolean addEncryptedKeyInKeyInfo) throws ProcessingException, ConfigurationException {
         String wrappingElementPrefix = wrappingElementQName.getPrefix();
-        if (wrappingElementPrefix == null || wrappingElementPrefix == "")
+        if (wrappingElementPrefix == null || "".equals(wrappingElementPrefix))
             throw logger.wrongTypeError("Wrapping element prefix invalid");
 
         XMLCipher cipher = null;
@@ -386,15 +326,19 @@ public class XMLEncryptionUtil {
         // The EncryptedKey element is added
         Element encryptedKeyElement = cipher.martial(document, encryptedKey);
 
-        String wrappingElementName = wrappingElementPrefix + ":" + wrappingElementQName.getLocalPart();
+        final String wrappingElementName;
 
+        if (StringUtil.isNullOrEmpty(wrappingElementPrefix)) {
+            wrappingElementName = wrappingElementQName.getLocalPart();
+        } else {
+            wrappingElementName = wrappingElementPrefix + ":" + wrappingElementQName.getLocalPart();
+        }
         // Create the wrapping element and set its attribute NS
         Element wrappingElement = encryptedDoc.createElementNS(wrappingElementQName.getNamespaceURI(), wrappingElementName);
 
-        if (StringUtil.isNullOrEmpty(wrappingElementPrefix)) {
-            wrappingElementName = wrappingElementQName.getLocalPart();
+        if (! StringUtil.isNullOrEmpty(wrappingElementPrefix)) {
+            wrappingElement.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, "xmlns:" + wrappingElementPrefix, wrappingElementQName.getNamespaceURI());
         }
-        wrappingElement.setAttributeNS(XMLNS, "xmlns:" + wrappingElementPrefix, wrappingElementQName.getNamespaceURI());
 
         Element encryptedDocRootElement = encryptedDoc.getDocumentElement();
         // Bring in the encrypted wrapping element to wrap the root node
@@ -404,12 +348,12 @@ public class XMLEncryptionUtil {
 
         if (addEncryptedKeyInKeyInfo) {
             // Outer ds:KeyInfo Element to hold the EncryptionKey
-            Element sigElement = encryptedDoc.createElementNS(XMLSIG_NS, DS_KEY_INFO);
-            sigElement.setAttributeNS(XMLNS, "xmlns:ds", XMLSIG_NS);
+            Element sigElement = encryptedDoc.createElementNS(XMLSignature.XMLNS, DS_KEY_INFO);
+            sigElement.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, "xmlns:ds", XMLSignature.XMLNS);
             sigElement.appendChild(encryptedKeyElement);
 
             // Insert the Encrypted key before the CipherData element
-            NodeList nodeList = encryptedDocRootElement.getElementsByTagNameNS(XMLENC_NS, CIPHER_DATA_LOCALNAME);
+            NodeList nodeList = encryptedDocRootElement.getElementsByTagNameNS(EncryptionConstants.EncryptionSpecNS, EncryptionConstants._TAG_CIPHERDATA);
             if (nodeList == null || nodeList.getLength() == 0)
                 throw logger.domMissingElementError("xenc:CipherData");
 
@@ -430,9 +374,6 @@ public class XMLEncryptionUtil {
      * @param privateKey key need to unwrap the encryption key
      *
      * @return the document with the encrypted element replaced by the data element
-     *
-     * @throws XMLEncryptionException
-     * @throws ProcessingException
      */
     public static Element decryptElementInDocument(Document documentWithEncryptedElement, PrivateKey privateKey)
             throws ProcessingException {
@@ -449,7 +390,7 @@ public class XMLEncryptionUtil {
         Element encKeyElement = getNextElementNode(encDataElement.getNextSibling());
         if (encKeyElement == null) {
             // Search the enc data element for enc key
-            NodeList nodeList = encDataElement.getElementsByTagNameNS(XMLENC_NS, ENCRYPTED_KEY_LOCALNAME);
+            NodeList nodeList = encDataElement.getElementsByTagNameNS(EncryptionConstants.EncryptionSpecNS, EncryptionConstants._TAG_ENCRYPTEDKEY);
 
             if (nodeList == null || nodeList.getLength() == 0)
                 throw logger.nullValueError("Encrypted Key not found in the enc data");
@@ -522,8 +463,6 @@ public class XMLEncryptionUtil {
         }
         if (publicKeyAlgo.contains("RSA"))
             return RSA_ENCRYPTION_SCHEME;
-        if (publicKeyAlgo.contains("DES"))
-            return XMLCipher.TRIPLEDES_KeyWrap;
         throw logger.unsupportedType("unsupported publicKey Algo:" + publicKeyAlgo);
     }
 
@@ -548,8 +487,6 @@ public class XMLEncryptionUtil {
         }
         if (algo.contains("RSA"))
             return XMLCipher.RSA_v1dot5;
-        if (algo.contains("DES"))
-            return XMLCipher.TRIPLEDES_KeyWrap;
         throw logger.unsupportedType("Secret Key with unsupported algo:" + algo);
     }
 
diff --git a/saml-core-api/src/main/java/org/keycloak/saml/common/constants/JBossSAMLURIConstants.java b/saml-core-api/src/main/java/org/keycloak/saml/common/constants/JBossSAMLURIConstants.java
index cf3b782..1479495 100755
--- a/saml-core-api/src/main/java/org/keycloak/saml/common/constants/JBossSAMLURIConstants.java
+++ b/saml-core-api/src/main/java/org/keycloak/saml/common/constants/JBossSAMLURIConstants.java
@@ -79,8 +79,9 @@ public enum JBossSAMLURIConstants {
             "http://www.w3.org/2000/09/xmldsig#rsa-sha1"),
 
     SAML_HTTP_POST_BINDING("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"),
-    SAML_HTTP_SOAP_BINDING("urn:oasis:names:tc:SAML:2.0:bindings:SOAP"),
     SAML_HTTP_REDIRECT_BINDING("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"),
+    SAML_SOAP_BINDING("urn:oasis:names:tc:SAML:2.0:bindings:SOAP"),
+    SAML_PAOS_BINDING("urn:oasis:names:tc:SAML:2.0:bindings:PAOS"),
 
     SAML_11_NS("urn:oasis:names:tc:SAML:1.0:assertion"),
 
diff --git a/server-spi/src/main/java/org/keycloak/models/RealmModel.java b/server-spi/src/main/java/org/keycloak/models/RealmModel.java
index e9df047..09720a7 100755
--- a/server-spi/src/main/java/org/keycloak/models/RealmModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/RealmModel.java
@@ -351,8 +351,6 @@ public interface RealmModel extends RoleContainerModel {
 
     void setNotBefore(int notBefore);
 
-    boolean removeRoleById(String id);
-
     boolean isEventsEnabled();
 
     void setEventsEnabled(boolean enabled);
@@ -397,13 +395,6 @@ public interface RealmModel extends RoleContainerModel {
     GroupModel createGroup(String name);
     GroupModel createGroup(String id, String name);
 
-    /**
-     * Move Group to top realm level.  Basically just sets group parent to null.  You need to call this though
-     * to make sure caches are set properly
-     *
-     * @param subGroup
-     */
-    void addTopLevelGroup(GroupModel subGroup);
     GroupModel getGroupById(String id);
     List<GroupModel> getGroups();
     List<GroupModel> getTopLevelGroups();
diff --git a/server-spi/src/main/java/org/keycloak/models/RoleMapperModel.java b/server-spi/src/main/java/org/keycloak/models/RoleMapperModel.java
index 85f1fd3..46c3198 100755
--- a/server-spi/src/main/java/org/keycloak/models/RoleMapperModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/RoleMapperModel.java
@@ -24,8 +24,17 @@ import java.util.Set;
  * @version $Revision: 1 $
  */
 public interface RoleMapperModel {
+    /**
+     * Returns set of realm roles that are directly set to this object.
+     * @return see description
+     */
     Set<RoleModel> getRealmRoleMappings();
 
+    /**
+     * Returns set of client roles that are directly set to this object for the given client.
+     * @param app Client to get the roles for
+     * @return see description
+     */
     Set<RoleModel> getClientRoleMappings(ClientModel app);
 
     /**
@@ -48,7 +57,15 @@ public interface RoleMapperModel {
      */
     void grantRole(RoleModel role);
 
+    /**
+     * Returns set of all role (both realm all client) that are directly set to this object.
+     * @return
+     */
     Set<RoleModel> getRoleMappings();
 
+    /**
+     * Removes the given role mapping from this object.
+     * @param role Role to remove
+     */
     void deleteRoleMapping(RoleModel role);
 }
diff --git a/server-spi/src/main/java/org/keycloak/models/UserManager.java b/server-spi/src/main/java/org/keycloak/models/UserManager.java
index 81b2b51..d606dfc 100755
--- a/server-spi/src/main/java/org/keycloak/models/UserManager.java
+++ b/server-spi/src/main/java/org/keycloak/models/UserManager.java
@@ -17,8 +17,6 @@
 
 package org.keycloak.models;
 
-import org.keycloak.models.session.UserSessionPersisterProvider;
-
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
  */
@@ -35,17 +33,25 @@ public class UserManager {
     }
 
     public boolean removeUser(RealmModel realm, UserModel user, UserProvider userProvider) {
-        UserSessionProvider sessions = session.sessions();
-        if (sessions != null) {
-            sessions.onUserRemoved(realm, user);
-        }
+        if (userProvider.removeUser(realm, user)) {
+            session.getKeycloakSessionFactory().publish(new UserModel.UserRemovedEvent() {
 
-        UserSessionPersisterProvider sessionsPersister = session.getProvider(UserSessionPersisterProvider.class);
-        if (sessionsPersister != null) {
-            sessionsPersister.onUserRemoved(realm, user);
-        }
+                @Override
+                public RealmModel getRealm() {
+                    return realm;
+                }
 
-        if (userProvider.removeUser(realm, user)) {
+                @Override
+                public UserModel getUser() {
+                    return user;
+                }
+
+                @Override
+                public KeycloakSession getKeycloakSession() {
+                    return session;
+                }
+
+            });
             return true;
         }
         return false;
diff --git a/server-spi/src/main/java/org/keycloak/models/UserModel.java b/server-spi/src/main/java/org/keycloak/models/UserModel.java
index 233c8a8..15cc296 100755
--- a/server-spi/src/main/java/org/keycloak/models/UserModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/UserModel.java
@@ -35,6 +35,7 @@ public interface UserModel extends RoleMapperModel {
     String LOCALE = "locale";
 
     interface UserRemovedEvent extends ProviderEvent {
+        RealmModel getRealm();
         UserModel getUser();
         KeycloakSession getKeycloakSession();
     }
diff --git a/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java b/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java
index 585558c..4102de1 100755
--- a/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java
+++ b/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java
@@ -55,7 +55,6 @@ public interface UserSessionProvider extends Provider {
 
     void onRealmRemoved(RealmModel realm);
     void onClientRemoved(RealmModel realm, ClientModel client);
-    void onUserRemoved(RealmModel realm, UserModel user);
 
     UserSessionModel createOfflineUserSession(UserSessionModel userSession);
     UserSessionModel getOfflineUserSession(RealmModel realm, String userSessionId);
diff --git a/server-spi-private/src/main/java/org/keycloak/cluster/ClusterListener.java b/server-spi-private/src/main/java/org/keycloak/cluster/ClusterListener.java
index 41c65c0..2c07377 100644
--- a/server-spi-private/src/main/java/org/keycloak/cluster/ClusterListener.java
+++ b/server-spi-private/src/main/java/org/keycloak/cluster/ClusterListener.java
@@ -29,6 +29,6 @@ public interface ClusterListener {
      *
      * @param event value of notification (Object added into the cache)
      */
-    void run(ClusterEvent event);
+    void eventReceived(ClusterEvent event);
 
 }
diff --git a/server-spi-private/src/main/java/org/keycloak/cluster/ClusterProvider.java b/server-spi-private/src/main/java/org/keycloak/cluster/ClusterProvider.java
index 6c22056..abed174 100644
--- a/server-spi-private/src/main/java/org/keycloak/cluster/ClusterProvider.java
+++ b/server-spi-private/src/main/java/org/keycloak/cluster/ClusterProvider.java
@@ -48,7 +48,8 @@ public interface ClusterProvider extends Provider {
 
 
     /**
-     * Register task (listener) under given key. When this key will be put to the cache on any cluster node, the task will be executed
+     * Register task (listener) under given key. When this key will be put to the cache on any cluster node, the task will be executed.
+     * When using {@link #ALL} as the taskKey, then listener will be always triggered for any value put into the cache.
      *
      * @param taskKey
      * @param task
@@ -57,10 +58,18 @@ public interface ClusterProvider extends Provider {
 
 
     /**
-     * Notify registered listeners on all cluster nodes
+     * Notify registered listeners on all cluster nodes. It will notify listeners registered under given taskKey AND also listeners registered with {@link #ALL} key (those are always executed)
      *
      * @param taskKey
      * @param event
+     * @param ignoreSender if true, then sender node itself won't receive the notification
      */
-    void notify(String taskKey, ClusterEvent event);
+    void notify(String taskKey, ClusterEvent event, boolean ignoreSender);
+
+
+    /**
+     * Special value to be used with {@link #registerListener}  to specify that particular listener will be always triggered for all notifications
+     * with any key.
+     */
+    String ALL = "ALL";
 }
diff --git a/server-spi-private/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java b/server-spi-private/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java
index 7bc1299..61ae1be 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java
@@ -27,12 +27,12 @@ public interface CacheRealmProvider extends RealmProvider {
     void clear();
     RealmProvider getDelegate();
 
-    void registerRealmInvalidation(String id);
+    void registerRealmInvalidation(String id, String name);
 
-    void registerClientInvalidation(String id);
+    void registerClientInvalidation(String id, String clientId, String realmId);
     void registerClientTemplateInvalidation(String id);
 
-    void registerRoleInvalidation(String id);
+    void registerRoleInvalidation(String id, String roleName, String roleContainerId);
 
     void registerGroupInvalidation(String id);
 }
diff --git a/server-spi-private/src/main/java/org/keycloak/models/session/UserSessionPersisterProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/models/session/UserSessionPersisterProviderFactory.java
index 2c0b98c..350f4f9 100644
--- a/server-spi-private/src/main/java/org/keycloak/models/session/UserSessionPersisterProviderFactory.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/session/UserSessionPersisterProviderFactory.java
@@ -17,10 +17,32 @@
 
 package org.keycloak.models.session;
 
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.UserModel;
+import org.keycloak.provider.ProviderEvent;
+import org.keycloak.provider.ProviderEventListener;
 import org.keycloak.provider.ProviderFactory;
 
 /**
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
  */
 public interface UserSessionPersisterProviderFactory extends ProviderFactory<UserSessionPersisterProvider> {
+
+    @Override
+    default void postInit(KeycloakSessionFactory factory) {
+        factory.register(new ProviderEventListener() {
+
+            @Override
+            public void onEvent(ProviderEvent event) {
+                if (event instanceof UserModel.UserRemovedEvent) {
+                    UserModel.UserRemovedEvent userRemovedEvent = (UserModel.UserRemovedEvent) event;
+
+                    UserSessionPersisterProvider provider = userRemovedEvent.getKeycloakSession().getProvider(UserSessionPersisterProvider.class, getId());
+                    provider.onUserRemoved(userRemovedEvent.getRealm(), userRemovedEvent.getUser());
+                }
+            }
+
+        });
+    }
+
 }
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/ConditionalOtpFormAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/ConditionalOtpFormAuthenticator.java
index ef17476..1696a1d 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/ConditionalOtpFormAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/ConditionalOtpFormAuthenticator.java
@@ -18,6 +18,9 @@
 package org.keycloak.authentication.authenticators.browser;
 
 import org.keycloak.authentication.AuthenticationFlowContext;
+import org.keycloak.models.AuthenticatorConfigModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
 import org.keycloak.models.RoleModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.utils.RoleUtils;
@@ -106,15 +109,15 @@ public class ConditionalOtpFormAuthenticator extends OTPFormAuthenticator {
 
         Map<String, String> config = context.getAuthenticatorConfig().getConfig();
 
-        if (tryConcludeBasedOn(voteForUserOtpControlAttribute(context, config), context)) {
+        if (tryConcludeBasedOn(voteForUserOtpControlAttribute(context.getUser(), config), context)) {
             return;
         }
 
-        if (tryConcludeBasedOn(voteForUserRole(context, config), context)) {
+        if (tryConcludeBasedOn(voteForUserRole(context.getRealm(), context.getUser(), config), context)) {
             return;
         }
 
-        if (tryConcludeBasedOn(voteForHttpHeaderMatchesPattern(context, config), context)) {
+        if (tryConcludeBasedOn(voteForHttpHeaderMatchesPattern(context.getHttpRequest().getHttpHeaders().getRequestHeaders(), config), context)) {
             return;
         }
 
@@ -158,11 +161,26 @@ public class ConditionalOtpFormAuthenticator extends OTPFormAuthenticator {
         }
     }
 
+    private boolean tryConcludeBasedOn(OtpDecision state) {
+
+        switch (state) {
+
+            case SHOW_OTP:
+                return true;
+
+            case SKIP_OTP:
+                return false;
+
+            default:
+                return false;
+        }
+    }
+
     private void showOtpForm(AuthenticationFlowContext context) {
         super.authenticate(context);
     }
 
-    private OtpDecision voteForUserOtpControlAttribute(AuthenticationFlowContext context, Map<String, String> config) {
+    private OtpDecision voteForUserOtpControlAttribute(UserModel user, Map<String, String> config) {
 
         if (!config.containsKey(OTP_CONTROL_USER_ATTRIBUTE)) {
             return ABSTAIN;
@@ -173,7 +191,7 @@ public class ConditionalOtpFormAuthenticator extends OTPFormAuthenticator {
             return ABSTAIN;
         }
 
-        List<String> values = context.getUser().getAttribute(attributeName);
+        List<String> values = user.getAttribute(attributeName);
 
         if (values.isEmpty()) {
             return ABSTAIN;
@@ -191,14 +209,12 @@ public class ConditionalOtpFormAuthenticator extends OTPFormAuthenticator {
         }
     }
 
-    private OtpDecision voteForHttpHeaderMatchesPattern(AuthenticationFlowContext context, Map<String, String> config) {
+    private OtpDecision voteForHttpHeaderMatchesPattern(MultivaluedMap<String, String> requestHeaders, Map<String, String> config) {
 
         if (!config.containsKey(FORCE_OTP_FOR_HTTP_HEADER) && !config.containsKey(SKIP_OTP_FOR_HTTP_HEADER)) {
             return ABSTAIN;
         }
 
-        MultivaluedMap<String, String> requestHeaders = context.getHttpRequest().getHttpHeaders().getRequestHeaders();
-
         //Inverted to allow white-lists, e.g. for specifying trusted remote hosts: X-Forwarded-Host: (1.2.3.4|1.2.3.5)
         if (containsMatchingRequestHeader(requestHeaders, config.get(SKIP_OTP_FOR_HTTP_HEADER))) {
             return SKIP_OTP;
@@ -238,32 +254,62 @@ public class ConditionalOtpFormAuthenticator extends OTPFormAuthenticator {
         return false;
     }
 
-    private OtpDecision voteForUserRole(AuthenticationFlowContext context, Map<String, String> config) {
+    private OtpDecision voteForUserRole(RealmModel realm, UserModel user, Map<String, String> config) {
 
         if (!config.containsKey(SKIP_OTP_ROLE) && !config.containsKey(FORCE_OTP_ROLE)) {
             return ABSTAIN;
         }
 
-        if (userHasRole(context, config.get(SKIP_OTP_ROLE))) {
+        if (userHasRole(realm, user, config.get(SKIP_OTP_ROLE))) {
             return SKIP_OTP;
         }
 
-        if (userHasRole(context, config.get(FORCE_OTP_ROLE))) {
+        if (userHasRole(realm, user, config.get(FORCE_OTP_ROLE))) {
             return SHOW_OTP;
         }
 
         return ABSTAIN;
     }
 
-    private boolean userHasRole(AuthenticationFlowContext context, String roleName) {
+    private boolean userHasRole(RealmModel realm, UserModel user, String roleName) {
 
         if (roleName == null) {
             return false;
         }
 
-        RoleModel role = getRoleFromString(context.getRealm(), roleName);
-        UserModel user = context.getUser();
+        RoleModel role = getRoleFromString(realm, roleName);
 
         return RoleUtils.hasRole(user.getRoleMappings(), role);
     }
+
+    private boolean isOTPRequired(KeycloakSession session, RealmModel realm, UserModel user) {
+        MultivaluedMap<String, String> requestHeaders = session.getContext().getRequestHeaders().getRequestHeaders();
+        for (AuthenticatorConfigModel configModel : realm.getAuthenticatorConfigs()) {
+
+            if (tryConcludeBasedOn(voteForUserOtpControlAttribute(user, configModel.getConfig()))) {
+                return true;
+            }
+            if (tryConcludeBasedOn(voteForUserRole(realm, user, configModel.getConfig()))) {
+                return true;
+            }
+            if (tryConcludeBasedOn(voteForHttpHeaderMatchesPattern(requestHeaders, configModel.getConfig()))) {
+                return true;
+            }
+            if (configModel.getConfig().get(DEFAULT_OTP_OUTCOME) != null
+                    && configModel.getConfig().get(DEFAULT_OTP_OUTCOME).equals(FORCE)
+                    && configModel.getConfig().size() <= 1) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
+        if (!isOTPRequired(session, realm, user)) {
+            user.removeRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP);
+        } else if (!user.getRequiredActions().contains(UserModel.RequiredAction.CONFIGURE_TOTP.name())) {
+            user.addRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP.name());
+        }
+    }
 }
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/OTPFormAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/OTPFormAuthenticator.java
index 9df33fc..9126689 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/OTPFormAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/OTPFormAuthenticator.java
@@ -37,8 +37,6 @@ import javax.ws.rs.core.Response;
  * @version $Revision: 1 $
  */
 public class OTPFormAuthenticator extends AbstractUsernameFormAuthenticator implements Authenticator {
-    public static final String TOTP_FORM_ACTION = "totp";
-
     @Override
     public void action(AuthenticationFlowContext context) {
         validateOTP(context);
@@ -99,8 +97,6 @@ public class OTPFormAuthenticator extends AbstractUsernameFormAuthenticator impl
 
     }
 
-
-
     @Override
     public void close() {
 
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LoginStatusIframeEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LoginStatusIframeEndpoint.java
index 5d2d054..605047f 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LoginStatusIframeEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LoginStatusIframeEndpoint.java
@@ -75,7 +75,7 @@ public class LoginStatusIframeEndpoint {
             if (client != null) {
                 Set<String> validWebOrigins = WebOriginsUtils.resolveValidWebOrigins(uriInfo, client);
                 validWebOrigins.add(UriUtils.getOrigin(uriInfo.getRequestUri()));
-                if (validWebOrigins.contains(origin)) {
+                if (validWebOrigins.contains("*") || validWebOrigins.contains(origin)) {
                     return Response.noContent().build();
                 }
             }
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractUserRoleMappingMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractUserRoleMappingMapper.java
index de4d054..4de3720 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractUserRoleMappingMapper.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractUserRoleMappingMapper.java
@@ -17,18 +17,19 @@
 
 package org.keycloak.protocol.oidc.mappers;
 
-import org.keycloak.models.ClientSessionModel;
-import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.GroupModel;
 import org.keycloak.models.ProtocolMapperModel;
 import org.keycloak.models.RoleModel;
+import org.keycloak.models.UserModel;
 import org.keycloak.models.UserSessionModel;
-import org.keycloak.representations.AccessToken;
 import org.keycloak.representations.IDToken;
 
 import java.util.ArrayDeque;
 import java.util.Deque;
-import java.util.LinkedHashSet;
 import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 /**
  * Base class for mapping of user role mappings to an ID and Access Token claim.
@@ -38,39 +39,95 @@ import java.util.Set;
 abstract class AbstractUserRoleMappingMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper, OIDCIDTokenMapper, UserInfoTokenMapper {
 
     /**
-     * Returns the role names extracted from the given {@code roleModels} while recursively traversing "Composite Roles".
-     * <p>
-     * Optionally prefixes each role name with the given {@code prefix}.
-     * </p>
-     *
-     * @param roleModels
-     * @param prefix     the prefix to apply, may be {@literal null}
+     * Returns a stream with roles that come from:
+     * <ul>
+     * <li>Direct assignment of the role to the user</li>
+     * <li>Direct assignment of the role to any group of the user or any of its parent group</li>
+     * <li>Composite roles are expanded recursively, the composite role itself is also contained in the returned stream</li>
+     * </ul>
+     * @param user User to enumerate the roles for
+     * @return
+     */
+    public static Stream<RoleModel> getAllUserRolesStream(UserModel user) {
+        return Stream.concat(
+          user.getRoleMappings().stream(),
+          user.getGroups().stream()
+            .flatMap(g -> groupAndItsParentsStream(g))
+            .flatMap(g -> g.getRoleMappings().stream()))
+          .flatMap(role -> expandCompositeRolesStream(role));
+    }
+
+    /**
+     * Returns stream of the given group and its parents (recursively).
+     * @param group
      * @return
      */
-    protected Set<String> flattenRoleModelToRoleNames(Set<RoleModel> roleModels, String prefix) {
+    private static Stream<GroupModel> groupAndItsParentsStream(GroupModel group) {
+        Stream.Builder<GroupModel> sb = Stream.builder();
+        while (group != null) {
+            sb.add(group);
+            group = group.getParent();
+        }
+        return sb.build();
+    }
 
-        Set<String> roleNames = new LinkedHashSet<>();
+    /**
+     * Recursively expands composite roles into their composite.
+     * @param role
+     * @return Stream of containing all of the composite roles and their components.
+     */
+    private static Stream<RoleModel> expandCompositeRolesStream(RoleModel role) {
+        Stream.Builder<RoleModel> sb = Stream.builder();
 
-        Deque<RoleModel> stack = new ArrayDeque<>(roleModels);
-        while (!stack.isEmpty()) {
+        Deque<RoleModel> stack = new ArrayDeque<>();
+        stack.add(role);
 
+        while (! stack.isEmpty()) {
             RoleModel current = stack.pop();
+            sb.add(current);
 
             if (current.isComposite()) {
-                for (RoleModel compositeRoleModel : current.getComposites()) {
-                    stack.push(compositeRoleModel);
-                }
+                stack.addAll(current.getComposites());
             }
+        }
 
-            String roleName = current.getName();
+        return sb.build();
+    }
 
-            if (prefix != null && !prefix.trim().isEmpty()) {
-                roleName = prefix.trim() + roleName;
-            }
+    /**
+     * Retrieves all roles of the current user based on direct roles set to the user, its groups and their parent groups.
+     * Then it recursively expands all composite roles, and restricts according to the given predicate {@code restriction}.
+     * If the current client sessions is restricted (i.e. no client found in active user session has full scope allowed),
+     * the final list of roles is also restricted by the client scope. Finally, the list is mapped to the token into
+     * a claim.
+     *
+     * @param token
+     * @param mappingModel
+     * @param userSession
+     * @param restriction
+     * @param prefix
+     */
+    protected static void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession,
+      Predicate<RoleModel> restriction, String prefix) {
+        String rolePrefix = prefix == null ? "" : prefix;
+        UserModel user = userSession.getUser();
+
+        // get a set of all realm roles assigned to the user or its group
+        Stream<RoleModel> clientUserRoles = getAllUserRolesStream(user).filter(restriction);
 
-            roleNames.add(roleName);
+        boolean dontLimitScope = userSession.getClientSessions().stream().anyMatch(cs -> cs.getClient().isFullScopeAllowed());
+        if (! dontLimitScope) {
+            Set<RoleModel> clientRoles = userSession.getClientSessions().stream()
+              .flatMap(cs -> cs.getClient().getScopeMappings().stream())
+              .collect(Collectors.toSet());
+
+            clientUserRoles = clientUserRoles.filter(clientRoles::contains);
         }
 
-        return roleNames;
+        Set<String> realmRoleNames = clientUserRoles
+          .map(m -> rolePrefix + m.getName())
+          .collect(Collectors.toSet());
+
+        OIDCAttributeMapperHelper.mapClaim(token, mappingModel, realmRoleNames);
     }
 }
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAttributeMapperHelper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAttributeMapperHelper.java
index 99b2610..8b64aef 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAttributeMapperHelper.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAttributeMapperHelper.java
@@ -100,6 +100,9 @@ public class OIDCAttributeMapperHelper {
         if (attributeValue == null) return;
 
         String protocolClaim = mappingModel.getConfig().get(TOKEN_CLAIM_NAME);
+        if (protocolClaim == null) {
+            return;
+        }
         String[] split = protocolClaim.split("\\.");
         Map<String, Object> jsonObject = token.getOtherClaims();
         for (int i = 0; i < split.length; i++) {
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserClientRoleMappingMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserClientRoleMappingMapper.java
index 01d47e1..5a88c2a 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserClientRoleMappingMapper.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserClientRoleMappingMapper.java
@@ -18,17 +18,20 @@
 package org.keycloak.protocol.oidc.mappers;
 
 import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientTemplateModel;
 import org.keycloak.models.ProtocolMapperModel;
+import org.keycloak.models.RealmModel;
 import org.keycloak.models.RoleModel;
-import org.keycloak.models.UserModel;
 import org.keycloak.models.UserSessionModel;
 import org.keycloak.protocol.ProtocolMapperUtils;
 import org.keycloak.provider.ProviderConfigProperty;
 import org.keycloak.representations.IDToken;
 
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.function.Predicate;
 
 /**
  * Allows mapping of user client role mappings to an ID and Access Token claim.
@@ -39,7 +42,7 @@ public class UserClientRoleMappingMapper extends AbstractUserRoleMappingMapper {
 
     public static final String PROVIDER_ID = "oidc-usermodel-client-role-mapper";
 
-    private static final List<ProviderConfigProperty> CONFIG_PROPERTIES = new ArrayList<ProviderConfigProperty>();
+    private static final List<ProviderConfigProperty> CONFIG_PROPERTIES = new ArrayList<>();
 
     static {
 
@@ -60,6 +63,7 @@ public class UserClientRoleMappingMapper extends AbstractUserRoleMappingMapper {
         OIDCAttributeMapperHelper.addAttributeConfig(CONFIG_PROPERTIES, UserClientRoleMappingMapper.class);
     }
 
+    @Override
     public List<ProviderConfigProperty> getConfigProperties() {
         return CONFIG_PROPERTIES;
     }
@@ -84,23 +88,51 @@ public class UserClientRoleMappingMapper extends AbstractUserRoleMappingMapper {
         return "Map a user client role to a token claim.";
     }
 
+    @Override
     protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession) {
+        String clientId = mappingModel.getConfig().get(ProtocolMapperUtils.USER_MODEL_CLIENT_ROLE_MAPPING_CLIENT_ID);
+        String rolePrefix = mappingModel.getConfig().get(ProtocolMapperUtils.USER_MODEL_CLIENT_ROLE_MAPPING_ROLE_PREFIX);
+
+        setClaim(token, mappingModel, userSession, getClientRoleFilter(clientId, userSession), rolePrefix);
+    }
 
-        UserModel user = userSession.getUser();
+    private static Predicate<RoleModel> getClientRoleFilter(String clientId, UserSessionModel userSession) {
+        if (clientId == null) {
+            return RoleModel::isClientRole;
+        }
 
-        String clientId = mappingModel.getConfig().get(ProtocolMapperUtils.USER_MODEL_CLIENT_ROLE_MAPPING_CLIENT_ID);
-        if (clientId != null) {
+        RealmModel clientRealm = userSession.getRealm();
+        ClientModel client = clientRealm.getClientByClientId(clientId.trim());
 
-            ClientModel clientModel = userSession.getRealm().getClientByClientId(clientId.trim());
-            Set<RoleModel> clientRoleMappings = user.getClientRoleMappings(clientModel);
+        if (client == null) {
+            return RoleModel::isClientRole;
+        }
 
-            String rolePrefix = mappingModel.getConfig().get(ProtocolMapperUtils.USER_MODEL_CLIENT_ROLE_MAPPING_ROLE_PREFIX);
-            Set<String> clientRoleNames = flattenRoleModelToRoleNames(clientRoleMappings, rolePrefix);
+        ClientTemplateModel template = client.getClientTemplate();
+        boolean useTemplateScope = template != null && client.useTemplateScope();
+        boolean fullScopeAllowed = (useTemplateScope && template.isFullScopeAllowed()) || client.isFullScopeAllowed();
 
-            OIDCAttributeMapperHelper.mapClaim(token, mappingModel, clientRoleNames);
+        Set<RoleModel> clientRoleMappings = client.getRoles();
+        if (fullScopeAllowed) {
+            return clientRoleMappings::contains;
+        }
+
+        Set<RoleModel> scopeMappings = new HashSet<>();
+
+        if (useTemplateScope) {
+            Set<RoleModel> templateScopeMappings = template.getScopeMappings();
+            if (templateScopeMappings != null) {
+                scopeMappings.addAll(templateScopeMappings);
+            }
         }
-    }
 
+        Set<RoleModel> clientScopeMappings = client.getScopeMappings();
+        if (clientScopeMappings != null) {
+            scopeMappings.addAll(clientScopeMappings);
+        }
+
+        return role -> clientRoleMappings.contains(role) && scopeMappings.contains(role);
+    }
 
     public static ProtocolMapperModel create(String clientId, String clientRolePrefix,
                                              String name,
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserRealmRoleMappingMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserRealmRoleMappingMapper.java
index ef98182..f978b08 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserRealmRoleMappingMapper.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserRealmRoleMappingMapper.java
@@ -18,18 +18,13 @@
 package org.keycloak.protocol.oidc.mappers;
 
 import org.keycloak.models.ProtocolMapperModel;
-import org.keycloak.models.UserModel;
 import org.keycloak.models.UserSessionModel;
 import org.keycloak.protocol.ProtocolMapperUtils;
-import org.keycloak.protocol.oidc.OIDCLoginProtocol;
 import org.keycloak.provider.ProviderConfigProperty;
 import org.keycloak.representations.IDToken;
 
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
-import java.util.Set;
 
 /**
  * Allows mapping of user realm role mappings to an ID and Access Token claim.
@@ -40,7 +35,7 @@ public class UserRealmRoleMappingMapper extends AbstractUserRoleMappingMapper {
 
     public static final String PROVIDER_ID = "oidc-usermodel-realm-role-mapper";
 
-    private static final List<ProviderConfigProperty> CONFIG_PROPERTIES = new ArrayList<ProviderConfigProperty>();
+    private static final List<ProviderConfigProperty> CONFIG_PROPERTIES = new ArrayList<>();
 
     static {
 
@@ -54,6 +49,7 @@ public class UserRealmRoleMappingMapper extends AbstractUserRoleMappingMapper {
         OIDCAttributeMapperHelper.addAttributeConfig(CONFIG_PROPERTIES, UserRealmRoleMappingMapper.class);
     }
 
+    @Override
     public List<ProviderConfigProperty> getConfigProperties() {
         return CONFIG_PROPERTIES;
     }
@@ -78,17 +74,12 @@ public class UserRealmRoleMappingMapper extends AbstractUserRoleMappingMapper {
         return "Map a user realm role to a token claim.";
     }
 
+    @Override
     protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession) {
-
-        UserModel user = userSession.getUser();
-
         String rolePrefix = mappingModel.getConfig().get(ProtocolMapperUtils.USER_MODEL_REALM_ROLE_MAPPING_ROLE_PREFIX);
-        Set<String> realmRoleNames = flattenRoleModelToRoleNames(user.getRealmRoleMappings(), rolePrefix);
-
-        OIDCAttributeMapperHelper.mapClaim(token, mappingModel, realmRoleNames);
+        AbstractUserRoleMappingMapper.setClaim(token, mappingModel, userSession, role -> ! role.isClientRole(), rolePrefix);
     }
 
-
     public static ProtocolMapperModel create(String realmRolePrefix,
                                              String name,
                                              String tokenClaimName, boolean accessToken, boolean idToken) {
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/WebOriginsUtils.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/WebOriginsUtils.java
index f606bfc..83f90f0 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/utils/WebOriginsUtils.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/WebOriginsUtils.java
@@ -21,6 +21,7 @@ import org.keycloak.common.util.UriUtils;
 import org.keycloak.models.ClientModel;
 
 import javax.ws.rs.core.UriInfo;
+import java.util.HashSet;
 import java.util.Set;
 
 /**
@@ -31,17 +32,20 @@ public class WebOriginsUtils {
     public static final String INCLUDE_REDIRECTS = "+";
 
     public static Set<String> resolveValidWebOrigins(UriInfo uriInfo, ClientModel client) {
-        Set<String> webOrigins = client.getWebOrigins();
-        if (webOrigins != null && webOrigins.contains("+")) {
-            webOrigins.remove(INCLUDE_REDIRECTS);
+        Set<String> origins = new HashSet<>();
+        if (client.getWebOrigins() != null) {
+            origins.addAll(client.getWebOrigins());
+        }
+        if (origins.contains("+")) {
+            origins.remove(INCLUDE_REDIRECTS);
             client.getRedirectUris();
             for (String redirectUri : RedirectUtils.resolveValidRedirects(uriInfo, client.getRootUrl(), client.getRedirectUris())) {
                 if (redirectUri.startsWith("http://") || redirectUri.startsWith("https://")) {
-                    webOrigins.add(UriUtils.getOrigin(redirectUri));
+                    origins.add(UriUtils.getOrigin(redirectUri));
                 }
             }
         }
-        return webOrigins;
+        return origins;
     }
 
 }
diff --git a/services/src/main/java/org/keycloak/protocol/saml/EntityDescriptorDescriptionConverter.java b/services/src/main/java/org/keycloak/protocol/saml/EntityDescriptorDescriptionConverter.java
index 3d62a27..d3cd904 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/EntityDescriptorDescriptionConverter.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/EntityDescriptorDescriptionConverter.java
@@ -123,6 +123,14 @@ public class EntityDescriptorDescriptionConverter implements ClientDescriptionCo
             attributes.put(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE, assertionConsumerServiceRedirectBinding);
             redirectUris.add(assertionConsumerServiceRedirectBinding);
         }
+        String assertionConsumerServiceSoapBinding = CoreConfigUtil.getServiceURL(spDescriptorType, JBossSAMLURIConstants.SAML_SOAP_BINDING.get());
+        if (assertionConsumerServiceSoapBinding != null) {
+            redirectUris.add(assertionConsumerServiceSoapBinding);
+        }
+        String assertionConsumerServicePaosBinding = CoreConfigUtil.getServiceURL(spDescriptorType, JBossSAMLURIConstants.SAML_PAOS_BINDING.get());
+        if (assertionConsumerServicePaosBinding != null) {
+            redirectUris.add(assertionConsumerServicePaosBinding);
+        }
         if (spDescriptorType.getNameIDFormat() != null) {
             for (String format : spDescriptorType.getNameIDFormat()) {
                 String attribute = SamlClient.samlNameIDFormatToClientAttribute(format);
diff --git a/services/src/main/java/org/keycloak/services/managers/UsersSyncManager.java b/services/src/main/java/org/keycloak/services/managers/UsersSyncManager.java
index d8c5dee..21b0cad 100755
--- a/services/src/main/java/org/keycloak/services/managers/UsersSyncManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/UsersSyncManager.java
@@ -155,7 +155,7 @@ public class UsersSyncManager {
     // Ensure all cluster nodes are notified
     public void notifyToRefreshPeriodicSync(KeycloakSession session, RealmModel realm, UserFederationProviderModel federationProvider, boolean removed) {
         FederationProviderClusterEvent event = FederationProviderClusterEvent.createEvent(removed, realm.getId(), federationProvider);
-        session.getProvider(ClusterProvider.class).notify(FEDERATION_TASK_KEY, event);
+        session.getProvider(ClusterProvider.class).notify(FEDERATION_TASK_KEY, event, false);
     }
 
 
@@ -265,7 +265,7 @@ public class UsersSyncManager {
         }
 
         @Override
-        public void run(ClusterEvent event) {
+        public void eventReceived(ClusterEvent event) {
             final FederationProviderClusterEvent fedEvent = (FederationProviderClusterEvent) event;
             KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
 
diff --git a/services/src/main/java/org/keycloak/services/managers/UserStorageSyncManager.java b/services/src/main/java/org/keycloak/services/managers/UserStorageSyncManager.java
index 05b07f2..b1114fa 100755
--- a/services/src/main/java/org/keycloak/services/managers/UserStorageSyncManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/UserStorageSyncManager.java
@@ -172,7 +172,7 @@ public class UserStorageSyncManager {
 
         }
         UserStorageProviderClusterEvent event = UserStorageProviderClusterEvent.createEvent(removed, realm.getId(), provider);
-        session.getProvider(ClusterProvider.class).notify(USER_STORAGE_TASK_KEY, event);
+        session.getProvider(ClusterProvider.class).notify(USER_STORAGE_TASK_KEY, event, false);
     }
 
 
@@ -282,7 +282,7 @@ public class UserStorageSyncManager {
         }
 
         @Override
-        public void run(ClusterEvent event) {
+        public void eventReceived(ClusterEvent event) {
             final UserStorageProviderClusterEvent fedEvent = (UserStorageProviderClusterEvent) event;
             KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
 
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java
index cd4d881..2b796c5 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java
@@ -319,7 +319,7 @@ public class AdminConsole {
     @Path("messages.json")
     @Produces(MediaType.APPLICATION_JSON)
     public Properties getMessages(@QueryParam("lang") String lang) {
-        return AdminRoot.getMessages(session, realm, "admin-messages", lang);
+        return AdminRoot.getMessages(session, realm, lang, "admin-messages");
     }
 
 }
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java
index b7dcddf..5db1ea4 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java
@@ -288,7 +288,16 @@ public class AdminRoot {
         }
     }
 
-    public static Properties getMessages(KeycloakSession session, RealmModel realm, String bundle, String lang) {
+    public static Properties getMessages(KeycloakSession session, RealmModel realm, String lang, String... bundles) {
+        Properties compound = new Properties();
+        for (String bundle : bundles) {
+            Properties current = getMessages(session, realm, lang, bundle);
+            compound.putAll(current);
+        }
+        return compound;
+    }
+
+    private static Properties getMessages(KeycloakSession session, RealmModel realm, String lang, String bundle) {
         try {
             Theme theme = getTheme(session, realm);
             Locale locale = lang != null ? Locale.forLanguageTag(lang) : Locale.ENGLISH;
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java b/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java
index 087022f..0128425 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java
@@ -251,7 +251,11 @@ public class AuthenticationManagementResource {
     @NoCache
     public void deleteFlow(@PathParam("id") String id) {
         auth.requireManage();
-
+        
+        deleteFlow(id, true);
+    }
+    
+    private void deleteFlow(String id, boolean isTopMostLevel) {
         AuthenticationFlowModel flow = realm.getAuthenticationFlowById(id);
         if (flow == null) {
             throw new NotFoundException("Could not find flow with id");
@@ -259,18 +263,17 @@ public class AuthenticationManagementResource {
         if (flow.isBuiltIn()) {
             throw new BadRequestException("Can't delete built in flow");
         }
+        
         List<AuthenticationExecutionModel> executions = realm.getAuthenticationExecutions(id);
         for (AuthenticationExecutionModel execution : executions) {
-        	if(execution.getFlowId() != null) {
-        		AuthenticationFlowModel nonTopLevelFlow = realm.getAuthenticationFlowById(execution.getFlowId());
-        		realm.removeAuthenticationFlow(nonTopLevelFlow);
-        	}
-        	realm.removeAuthenticatorExecution(execution);
+            if(execution.getFlowId() != null) {
+                deleteFlow(execution.getFlowId(), false);
+            }
         }
         realm.removeAuthenticationFlow(flow);
 
         // Use just one event for top-level flow. Using separate events won't work properly for flows of depth 2 or bigger
-        adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo).success();
+        if (isTopMostLevel) adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo).success();
     }
 
     /**
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ComponentResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ComponentResource.java
index 56c7ce7..d3ac358 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/ComponentResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ComponentResource.java
@@ -188,7 +188,7 @@ public class ComponentResource {
     }
 
     private Response localizedErrorResponse(ComponentValidationException cve) {
-        Properties messages = AdminRoot.getMessages(session, realm, "admin-messages", auth.getAuth().getToken().getLocale());
+        Properties messages = AdminRoot.getMessages(session, realm, auth.getAuth().getToken().getLocale(), "admin-messages", "messages");
 
         Object[] localizedParameters = cve.getParameters()==null ? null : Arrays.asList(cve.getParameters()).stream().map((Object parameter) -> {
 
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UserFederationProvidersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UserFederationProvidersResource.java
index 8854a7b..fa1e139 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/UserFederationProvidersResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/UserFederationProvidersResource.java
@@ -21,6 +21,7 @@ import org.jboss.resteasy.annotations.cache.NoCache;
 import org.jboss.resteasy.spi.NotFoundException;
 import org.jboss.resteasy.spi.ResteasyProviderFactory;
 import org.keycloak.common.constants.KerberosConstants;
+import org.keycloak.component.ComponentModel;
 import org.keycloak.events.admin.OperationType;
 import org.keycloak.events.admin.ResourceType;
 import org.keycloak.mappers.FederationConfigValidationException;
@@ -60,8 +61,10 @@ import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriInfo;
 import java.text.MessageFormat;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 import java.util.Properties;
 
 /**
@@ -263,6 +266,33 @@ public class UserFederationProvidersResource {
         return instanceResource;
     }
 
+    // TODO: This endpoint exists, so that admin console can lookup userFederation provider OR userStorage provider by federationLink.
+    // TODO: Endpoint should be removed once UserFederation SPI is removed as fallback is not needed anymore than
+    @GET
+    @Path("instances-with-fallback/{id}")
+    @Produces(MediaType.APPLICATION_JSON)
+    @NoCache
+    public Map<String, String> getUserFederationInstanceWithFallback(@PathParam("id") String id) {
+        this.auth.requireView();
+
+        Map<String, String> result = new HashMap<>();
+        UserFederationProviderModel model = KeycloakModelUtils.findUserFederationProviderById(id, realm);
+        if (model != null) {
+            result.put("federationLinkName", model.getDisplayName());
+            result.put("federationLink", "#/realms/" + realm.getName() + "/user-federation/providers/" + model.getProviderName() + "/" + model.getId());
+            return result;
+        } else {
+            ComponentModel userStorage = KeycloakModelUtils.findUserStorageProviderById(id, realm);
+            if (userStorage != null) {
+                result.put("federationLinkName", userStorage.getName());
+                result.put("federationLink", "#/realms/" + realm.getName() + "/user-storage/providers/" + userStorage.getProviderId() + "/" + userStorage.getId());
+                return result;
+            } else {
+                throw new NotFoundException("Could not find federation provider or userStorage provider");
+            }
+        }
+    }
+
 
     private ConfigPropertyRepresentation toConfigPropertyRepresentation(ProviderConfigProperty prop) {
         return ModelToRepresentation.toRepresentation(prop);
diff --git a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
index 363d6f4..2ea9992 100644
--- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
+++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
@@ -51,6 +51,7 @@ import org.keycloak.services.resources.admin.AdminRoot;
 import org.keycloak.services.scheduled.ClearExpiredEvents;
 import org.keycloak.services.scheduled.ClearExpiredUserSessions;
 import org.keycloak.services.scheduled.ClusterAwareScheduledTaskRunner;
+import org.keycloak.services.scheduled.ScheduledTaskRunner;
 import org.keycloak.services.util.JsonConfigProvider;
 import org.keycloak.services.util.ObjectMapperResolver;
 import org.keycloak.timer.TimerProvider;
@@ -321,7 +322,7 @@ public class KeycloakApplication extends Application {
         try {
             TimerProvider timer = session.getProvider(TimerProvider.class);
             timer.schedule(new ClusterAwareScheduledTaskRunner(sessionFactory, new ClearExpiredEvents(), interval), interval, "ClearExpiredEvents");
-            timer.schedule(new ClusterAwareScheduledTaskRunner(sessionFactory, new ClearExpiredUserSessions(), interval), interval, "ClearExpiredUserSessions");
+            timer.schedule(new ScheduledTaskRunner(sessionFactory, new ClearExpiredUserSessions()), interval, "ClearExpiredUserSessions");
             new UsersSyncManager().bootstrapPeriodic(sessionFactory, timer);
             new UserStorageSyncManager().bootstrapPeriodic(sessionFactory, timer);
         } finally {
diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml
index 873d99d..8a2dc4b 100755
--- a/testsuite/integration/pom.xml
+++ b/testsuite/integration/pom.xml
@@ -233,6 +233,10 @@
             <artifactId>infinispan-core</artifactId>
         </dependency>
         <dependency>
+            <groupId>org.infinispan</groupId>
+            <artifactId>infinispan-cachestore-remote</artifactId>
+        </dependency>
+        <dependency>
             <groupId>org.seleniumhq.selenium</groupId>
             <artifactId>selenium-java</artifactId>
         </dependency>
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperSyncTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperSyncTest.java
index f5cbccb..91075ae 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperSyncTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperSyncTest.java
@@ -238,7 +238,7 @@ public class LDAPGroupMapperSyncTest {
             GroupModel model1 = realm.createGroup("model1");
             realm.moveGroup(model1, null);
             GroupModel model2 = realm.createGroup("model2");
-            kcGroup1.addChild(model2);
+            realm.moveGroup(model2, kcGroup1);
 
             // Sync groups again from LDAP. Nothing deleted
             syncResult = new GroupLDAPStorageMapperFactory().create(session, mapperModel).syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm);
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ClusterInvalidationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ClusterInvalidationTest.java
new file mode 100644
index 0000000..f71d0da
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ClusterInvalidationTest.java
@@ -0,0 +1,420 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.model;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.infinispan.Cache;
+import org.infinispan.notifications.Listener;
+import org.infinispan.notifications.cachelistener.annotation.CacheEntryRemoved;
+import org.infinispan.notifications.cachelistener.event.CacheEntryRemovedEvent;
+import org.jboss.logging.Logger;
+import org.junit.Assert;
+import org.junit.ClassRule;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientTemplateModel;
+import org.keycloak.models.GroupModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.UserConsentModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.services.managers.RealmManager;
+import org.keycloak.testsuite.KeycloakServer;
+import org.keycloak.testsuite.rule.KeycloakRule;
+import org.keycloak.testsuite.util.cli.TestCacheUtils;
+
+/**
+ * Requires execution with cluster (or external JDG) enabled and real database, which will be shared for both cluster nodes. Everything set by system properties:
+ *
+ * 1) Use those system properties to run against shared MySQL:
+ *
+ *  -Dkeycloak.connectionsJpa.url=jdbc:mysql://localhost/keycloak -Dkeycloak.connectionsJpa.driver=com.mysql.jdbc.Driver -Dkeycloak.connectionsJpa.user=keycloak
+ *  -Dkeycloak.connectionsJpa.password=keycloak
+ *
+ *
+ * 2) Then either choose from:
+ *
+ * 2.a) Run test with 2 keycloak nodes in cluster. Add this system property for that: -Dkeycloak.connectionsInfinispan.clustered=true
+ *
+ * 2.b) Run test with 2 keycloak nodes without cluster, but instead with external JDG. Both keycloak servers will send invalidation events to the JDG server and receive the events from this JDG server.
+ * They don't communicate with each other. So JDG is man-in-the-middle.
+ *
+ * This assumes that you have JDG 7.0 server running on localhost with HotRod endpoint on port 11222 (which is default port anyway).
+ *
+ * You also need to have this cache configured in JDG_HOME/standalone/configuration/standalone.xml to infinispan subsystem :
+ *
+ *  <local-cache name="work" start="EAGER" batching="false" />
+ *
+ * Finally, add this system property when running the test: -Dkeycloak.connectionsInfinispan.remoteStoreEnabled=true
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@Ignore
+public class ClusterInvalidationTest {
+
+    protected static final Logger logger = Logger.getLogger(ClusterInvalidationTest.class);
+
+    private static final String REALM_NAME = "test";
+
+    private static final int SLEEP_TIME_MS = Integer.parseInt(System.getProperty("sleep.time", "500"));
+
+    private static TestListener listener1realms;
+    private static TestListener listener1users;
+    private static TestListener listener2realms;
+    private static TestListener listener2users;
+
+    @ClassRule
+    public static KeycloakRule server1 = new KeycloakRule(new KeycloakRule.KeycloakSetup() {
+
+        @Override
+        public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+            InfinispanConnectionProvider infinispan = manager.getSession().getProvider(InfinispanConnectionProvider.class);
+
+            Cache cache = infinispan.getCache(InfinispanConnectionProvider.REALM_CACHE_NAME);
+            listener1realms = new TestListener("server1 - realms", cache);
+            cache.addListener(listener1realms);
+
+            cache = infinispan.getCache(InfinispanConnectionProvider.USER_CACHE_NAME);
+            listener1users = new TestListener("server1 - users", cache);
+            cache.addListener(listener1users);
+        }
+
+    });
+
+    @ClassRule
+    public static KeycloakRule server2 = new KeycloakRule(new KeycloakRule.KeycloakSetup() {
+
+        @Override
+        public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+            InfinispanConnectionProvider infinispan = manager.getSession().getProvider(InfinispanConnectionProvider.class);
+
+            Cache cache = infinispan.getCache(InfinispanConnectionProvider.REALM_CACHE_NAME);
+            listener2realms = new TestListener("server2 - realms", cache);
+            cache.addListener(listener2realms);
+
+            cache = infinispan.getCache(InfinispanConnectionProvider.USER_CACHE_NAME);
+            listener2users = new TestListener("server2 - users", cache);
+            cache.addListener(listener2users);
+        }
+
+    }) {
+
+        @Override
+        protected void configureServer(KeycloakServer server) {
+            server.getConfig().setPort(8082);
+        }
+
+        @Override
+        protected void importRealm() {
+        }
+
+        @Override
+        protected void removeTestRealms() {
+        }
+
+    };
+
+    private static void clearListeners() {
+        listener1realms.getInvalidationsAndClear();
+        listener1users.getInvalidationsAndClear();
+        listener2realms.getInvalidationsAndClear();
+        listener2users.getInvalidationsAndClear();
+    }
+
+
+    @Test
+    public void testClusterInvalidation() throws Exception {
+        cacheEverything();
+
+        clearListeners();
+
+        KeycloakSession session1 = server1.startSession();
+
+
+        logger.info("UPDATE REALM");
+
+        RealmModel realm = session1.realms().getRealmByName(REALM_NAME);
+        realm.setDisplayName("foo");
+        session1 = commit(server1, session1, true);
+
+        assertInvalidations(listener1realms.getInvalidationsAndClear(), 1, 3, realm.getId());
+        assertInvalidations(listener2realms.getInvalidationsAndClear(), 1, 3, realm.getId());
+
+
+        // CREATES
+
+        logger.info("CREATE ROLE");
+        realm = session1.realms().getRealmByName(REALM_NAME);
+        realm.addRole("foo-role");
+        session1 = commit(server1, session1, true);
+
+        assertInvalidations(listener1realms.getInvalidationsAndClear(), 1, 1, "test.roles");
+        assertInvalidations(listener2realms.getInvalidationsAndClear(), 1, 1, "test.roles");
+
+
+        logger.info("CREATE CLIENT");
+        realm = session1.realms().getRealmByName(REALM_NAME);
+        realm.addClient("foo-client");
+        session1 = commit(server1, session1, true);
+
+        assertInvalidations(listener1realms.getInvalidationsAndClear(), 1, 1, "test.realm.clients");
+        assertInvalidations(listener2realms.getInvalidationsAndClear(), 1, 1, "test.realm.clients");
+
+        logger.info("CREATE GROUP");
+        realm = session1.realms().getRealmByName(REALM_NAME);
+        GroupModel group = realm.createGroup("foo-group");
+        session1 = commit(server1, session1, true);
+
+        assertInvalidations(listener1realms.getInvalidationsAndClear(), 1, 1, "test.top.groups");
+        assertInvalidations(listener2realms.getInvalidationsAndClear(), 1, 1, "test.top.groups");
+
+        logger.info("CREATE CLIENT TEMPLATE");
+        realm = session1.realms().getRealmByName(REALM_NAME);
+        realm.addClientTemplate("foo-template");
+        session1 = commit(server1, session1, true);
+
+        assertInvalidations(listener1realms.getInvalidationsAndClear(), 2, 3, realm.getId());
+        assertInvalidations(listener2realms.getInvalidationsAndClear(), 0, 2); // realm not cached on server2 due to previous invalidation
+
+
+        // UPDATES
+
+        logger.info("UPDATE ROLE");
+        realm = session1.realms().getRealmByName(REALM_NAME);
+        ClientModel testApp = realm.getClientByClientId("test-app");
+        RoleModel role = session1.realms().getClientRole(realm, testApp, "customer-user");
+        role.setDescription("Foo");
+        session1 = commit(server1, session1, true);
+
+        assertInvalidations(listener1realms.getInvalidationsAndClear(), 2, 3, role.getId());
+        assertInvalidations(listener2realms.getInvalidationsAndClear(), 2, 3, role.getId());
+
+        logger.info("UPDATE GROUP");
+        realm = session1.realms().getRealmByName(REALM_NAME);
+        group = KeycloakModelUtils.findGroupByPath(realm, "/topGroup");
+        group.grantRole(role);
+        session1 = commit(server1, session1, true);
+
+        assertInvalidations(listener1realms.getInvalidationsAndClear(), 1, 1, group.getId());
+        assertInvalidations(listener2realms.getInvalidationsAndClear(), 1, 1, group.getId());
+
+        logger.info("UPDATE CLIENT");
+        realm = session1.realms().getRealmByName(REALM_NAME);
+        testApp = realm.getClientByClientId("test-app");
+        testApp.setDescription("foo");;
+        session1 = commit(server1, session1, true);
+
+        assertInvalidations(listener1realms.getInvalidationsAndClear(), 2, 3, testApp.getId());
+        assertInvalidations(listener2realms.getInvalidationsAndClear(), 2, 3, testApp.getId());
+
+        // Cache client template on server2
+        KeycloakSession session2 = server2.startSession();
+        realm = session2.realms().getRealmByName(REALM_NAME);
+        realm.getClientTemplates().get(0);
+
+
+        logger.info("UPDATE CLIENT TEMPLATE");
+        realm = session1.realms().getRealmByName(REALM_NAME);
+        ClientTemplateModel clientTemplate = realm.getClientTemplates().get(0);
+        clientTemplate.setDescription("bar");
+
+        session1 = commit(server1, session1, true);
+
+        assertInvalidations(listener1realms.getInvalidationsAndClear(), 1, 1, clientTemplate.getId());
+        assertInvalidations(listener2realms.getInvalidationsAndClear(), 1, 1, clientTemplate.getId());
+
+        // Nothing yet invalidated in user cache
+        assertInvalidations(listener1users.getInvalidationsAndClear(), 0, 0);
+        assertInvalidations(listener2users.getInvalidationsAndClear(), 0, 0);
+
+        logger.info("UPDATE USER");
+        realm = session1.realms().getRealmByName(REALM_NAME);
+        UserModel user = session1.users().getUserByEmail("keycloak-user@localhost", realm);
+        user.setSingleAttribute("foo", "Bar");
+        session1 = commit(server1, session1, true);
+
+        assertInvalidations(listener1users.getInvalidationsAndClear(), 1, 5, user.getId(), "test.email.keycloak-user@localhost");
+        assertInvalidations(listener2users.getInvalidationsAndClear(), 1, 5, user.getId());
+
+        logger.info("UPDATE USER CONSENTS");
+        realm = session1.realms().getRealmByName(REALM_NAME);
+        testApp = realm.getClientByClientId("test-app");
+        user = session1.users().getUserByEmail("keycloak-user@localhost", realm);
+        session1.users().addConsent(realm, user.getId(), new UserConsentModel(testApp));
+        session1 = commit(server1, session1, true);
+
+        assertInvalidations(listener1users.getInvalidationsAndClear(), 1, 1, user.getId() + ".consents");
+        assertInvalidations(listener2users.getInvalidationsAndClear(), 1, 1, user.getId() + ".consents");
+
+
+        // REMOVALS
+
+        logger.info("REMOVE USER");
+        realm = session1.realms().getRealmByName(REALM_NAME);
+        user = session1.users().getUserByUsername("john-doh@localhost", realm);
+        session1.users().removeUser(realm, user);
+
+        session1 = commit(server1, session1, true);
+
+        assertInvalidations(listener1users.getInvalidationsAndClear(), 3, 5, user.getId(), user.getId() + ".consents", "test.username.john-doh@localhost");
+        assertInvalidations(listener2users.getInvalidationsAndClear(), 2, 5, user.getId(), user.getId() + ".consents");
+
+        cacheEverything();
+
+        logger.info("REMOVE CLIENT TEMPLATE");
+        realm = session1.realms().getRealmByName(REALM_NAME);
+        realm.removeClientTemplate(clientTemplate.getId());
+        session1 = commit(server1, session1, true);
+
+        assertInvalidations(listener1realms.getInvalidationsAndClear(), 2, 5, realm.getId(), clientTemplate.getId());
+        assertInvalidations(listener2realms.getInvalidationsAndClear(), 2, 5, realm.getId(), clientTemplate.getId());
+
+        cacheEverything();
+
+        logger.info("REMOVE ROLE");
+        realm = session1.realms().getRealmByName(REALM_NAME);
+        role = realm.getRole("user");
+        realm.removeRole(role);
+        ClientModel thirdparty = session1.realms().getClientByClientId("third-party", realm);
+        session1 = commit(server1, session1, true);
+
+        assertInvalidations(listener1realms.getInvalidationsAndClear(), 7, 10, role.getId(), realm.getId(), "test.roles", "test.user.roles", testApp.getId(), thirdparty.getId(), group.getId());
+        assertInvalidations(listener2realms.getInvalidationsAndClear(), 7, 10, role.getId(), realm.getId(), "test.roles", "test.user.roles", testApp.getId(), thirdparty.getId(), group.getId());
+
+        // all users invalidated
+        assertInvalidations(listener1users.getInvalidationsAndClear(), 10, 100);
+        assertInvalidations(listener2users.getInvalidationsAndClear(), 10, 100);
+
+        cacheEverything();
+
+        logger.info("REMOVE GROUP");
+        realm = session1.realms().getRealmByName(REALM_NAME);
+        group = realm.getGroupById(group.getId());
+        String subgroupId = group.getSubGroups().iterator().next().getId();
+        realm.removeGroup(group);
+        session1 = commit(server1, session1, true);
+
+        assertInvalidations(listener1realms.getInvalidationsAndClear(), 3, 5, group.getId(), subgroupId, "test.top.groups");
+        assertInvalidations(listener2realms.getInvalidationsAndClear(), 3, 5, group.getId(), subgroupId, "test.top.groups");
+
+        // all users invalidated
+        assertInvalidations(listener1users.getInvalidationsAndClear(), 10, 100);
+        assertInvalidations(listener2users.getInvalidationsAndClear(), 10, 100);
+
+        cacheEverything();
+
+        logger.info("REMOVE CLIENT");
+        realm = session1.realms().getRealmByName(REALM_NAME);
+        testApp = realm.getClientByClientId("test-app");
+        role = testApp.getRole("customer-user");
+        realm.removeClient(testApp.getId());
+        session1 = commit(server1, session1, true);
+
+        assertInvalidations(listener1realms.getInvalidationsAndClear(), 8, 12, testApp.getId(), testApp.getId() + ".roles", role.getId(), testApp.getId() + ".customer-user.roles", "test.realm.clients", thirdparty.getId());
+        assertInvalidations(listener2realms.getInvalidationsAndClear(), 8, 12, testApp.getId(), testApp.getId() + ".roles", role.getId(), testApp.getId() + ".customer-user.roles", "test.realm.clients", thirdparty.getId());
+
+        // all users invalidated
+        assertInvalidations(listener1users.getInvalidationsAndClear(), 10, 100);
+        assertInvalidations(listener2users.getInvalidationsAndClear(), 10, 100);
+
+        cacheEverything();
+
+        logger.info("REMOVE REALM");
+        realm = session1.realms().getRealmByName(REALM_NAME);
+        session1.realms().removeRealm(realm.getId());
+        session1 = commit(server1, session1, true);
+
+        assertInvalidations(listener1realms.getInvalidationsAndClear(), 50, 200, realm.getId(), thirdparty.getId());
+        assertInvalidations(listener2realms.getInvalidationsAndClear(), 50, 200, realm.getId(), thirdparty.getId());
+
+        // all users invalidated
+        assertInvalidations(listener1users.getInvalidationsAndClear(), 10, 100);
+        assertInvalidations(listener2users.getInvalidationsAndClear(), 10, 100);
+
+
+        //Thread.sleep(10000000);
+    }
+
+    private void assertInvalidations(Map<String, Object> invalidations, int low, int high, String... expectedNames) {
+        int size = invalidations.size();
+        Assert.assertTrue("Size was " + size + ". Entries were: " + invalidations.keySet(), size >= low);
+        Assert.assertTrue("Size was " + size + ". Entries were: " + invalidations.keySet(), size <= high);
+
+        for (String expected : expectedNames) {
+            Assert.assertTrue("Can't find " + expected + ". Entries were: " + invalidations.keySet(), invalidations.keySet().contains(expected));
+        }
+    }
+
+    private KeycloakSession commit(KeycloakRule rule, KeycloakSession session, boolean sleepAfterCommit) throws Exception {
+        session.getTransactionManager().commit();
+        session.close();
+
+        if (sleepAfterCommit) {
+            Thread.sleep(SLEEP_TIME_MS);
+        }
+
+        return rule.startSession();
+    }
+
+    private void cacheEverything() throws Exception {
+        KeycloakSession session1 = server1.startSession();
+        TestCacheUtils.cacheRealmWithEverything(session1, REALM_NAME);
+        session1 = commit(server1, session1, false);
+
+        KeycloakSession session2 = server2.startSession();
+        TestCacheUtils.cacheRealmWithEverything(session2, REALM_NAME);
+        session2 = commit(server1, session2, false);
+    }
+
+
+    @Listener(observation = Listener.Observation.PRE)
+    public static class TestListener {
+
+        private final String name;
+        private final Cache cache; // Just for debugging
+
+        private Map<String, Object> invalidations = new ConcurrentHashMap<>();
+
+        public TestListener(String name, Cache cache) {
+            this.name = name;
+            this.cache = cache;
+        }
+
+        @CacheEntryRemoved
+        public void cacheEntryRemoved(CacheEntryRemovedEvent event) {
+            logger.infof("%s: Invalidated %s: %s", name, event.getKey(), event.getValue());
+            invalidations.put(event.getKey().toString(), event.getValue());
+        }
+
+        Map<String, Object> getInvalidationsAndClear() {
+            Map<String, Object> newMap = new HashMap<>(invalidations);
+            invalidations.clear();
+            return newMap;
+        }
+
+    }
+
+
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java
index 80f663f..60d92d9 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java
@@ -336,6 +336,7 @@ public class UserSessionPersisterProviderTest {
 
         resetSession();
 
+        Assert.assertEquals(1, persister.getUserSessionsCount(true));
         loadedSessions = loadPersistedSessionsPaginated(true, 10, 1, 1);
         UserSessionModel persistedSession = loadedSessions.get(0);
         UserSessionProviderTest.assertSession(persistedSession, session.users().getUserByUsername("user2", realm), "127.0.0.3", started, started, "test-app");
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java
index f2fc3aa..824200d 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java
@@ -74,8 +74,12 @@ public class UserSessionProviderTest {
         UserModel user2 = session.users().getUserByUsername("user2", realm);
 
         UserManager um = new UserManager(session);
-        um.removeUser(realm, user1);
-        um.removeUser(realm, user2);
+        if (user1 != null) {
+            um.removeUser(realm, user1);
+        }
+        if (user2 != null) {
+            um.removeUser(realm, user2);
+        }
         kc.stopSession(session, true);
     }
 
@@ -528,11 +532,12 @@ public class UserSessionProviderTest {
 
         resetSession();
 
-        session.sessions().onUserRemoved(realm, session.users().getUserByUsername("user1", realm));
+        UserModel user1 = session.users().getUserByUsername("user1", realm);
+        new UserManager(session).removeUser(realm, user1);
 
         resetSession();
 
-        assertTrue(session.sessions().getUserSessions(realm, session.users().getUserByUsername("user1", realm)).isEmpty());
+        assertTrue(session.sessions().getUserSessions(realm, user1).isEmpty());
         assertFalse(session.sessions().getUserSessions(realm, session.users().getUserByUsername("user2", realm)).isEmpty());
 
         assertNull(session.sessions().getUserLoginFailure(realm, "user1"));
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/CacheCommands.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/CacheCommands.java
new file mode 100644
index 0000000..0c7eff0
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/CacheCommands.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.util.cli;
+
+import java.util.Map;
+import java.util.Set;
+
+import org.infinispan.Cache;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class CacheCommands {
+
+    public static class ListCachesCommand extends AbstractCommand {
+
+        @Override
+        public String getName() {
+            return "listCaches";
+        }
+
+        @Override
+        protected void doRunCommand(KeycloakSession session) {
+            InfinispanConnectionProvider ispnProvider = session.getProvider(InfinispanConnectionProvider.class);
+            Set<String> cacheNames = ispnProvider.getCache("realms").getCacheManager().getCacheNames();
+            log.infof("Available caches: %s", cacheNames);
+        }
+
+    }
+
+
+    public static class GetCacheCommand extends AbstractCommand {
+
+        @Override
+        public String getName() {
+            return "getCache";
+        }
+
+        @Override
+        protected void doRunCommand(KeycloakSession session) {
+            String cacheName = getArg(0);
+            InfinispanConnectionProvider ispnProvider = session.getProvider(InfinispanConnectionProvider.class);
+            Cache<Object, Object> cache = ispnProvider.getCache(cacheName);
+            if (cache == null) {
+                log.errorf("Cache '%s' doesn't exist", cacheName);
+                throw new HandledException();
+            }
+
+            printCache(cache);
+        }
+
+        private void printCache(Cache<Object, Object> cache) {
+            int size = cache.size();
+            log.infof("Cache %s, size: %d", cache.getName(), size);
+
+            if (size > 50) {
+                log.info("Skip printing cache recors due to big size");
+            } else {
+                for (Map.Entry<Object, Object> entry : cache.entrySet()) {
+                    log.infof("%s=%s", entry.getKey(), entry.getValue());
+                }
+            }
+        }
+
+        @Override
+        public String printUsage() {
+            return super.printUsage() + " <cache-name> . cache-name is name of the infinispan cache provided by InfinispanConnectionProvider";
+        }
+
+    }
+
+
+    public static class CacheRealmObjectsCommand extends AbstractCommand {
+
+        @Override
+        public String getName() {
+            return "cacheRealmObjects";
+        }
+
+        @Override
+        protected void doRunCommand(KeycloakSession session) {
+            String realmName = getArg(0);
+            RealmModel realm = session.realms().getRealmByName(realmName);
+            if (realm == null) {
+                log.errorf("Realm not found: %s", realmName);
+                throw new HandledException();
+            }
+
+            TestCacheUtils.cacheRealmWithEverything(session, realmName);
+        }
+
+        @Override
+        public String printUsage() {
+            return super.printUsage() + " <realm-name>";
+        }
+    }
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/RoleCommands.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/RoleCommands.java
new file mode 100644
index 0000000..2c71c72
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/RoleCommands.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.util.cli;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionTask;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleContainerModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class RoleCommands {
+
+    public static class CreateRoles extends AbstractCommand {
+
+        private String rolePrefix;
+        private String roleContainer;
+
+        @Override
+        public String getName() {
+            return "createRoles";
+        }
+
+        private class StateHolder {
+            int firstInThisBatch;
+            int countInThisBatch;
+            int remaining;
+        };
+
+        @Override
+        protected void doRunCommand(KeycloakSession session) {
+            rolePrefix = getArg(0);
+            roleContainer = getArg(1);
+            int first = getIntArg(2);
+            int count = getIntArg(3);
+            int batchCount = getIntArg(4);
+
+            final StateHolder state = new StateHolder();
+            state.firstInThisBatch = first;
+            state.remaining = count;
+            state.countInThisBatch = Math.min(batchCount, state.remaining);
+            while (state.remaining > 0) {
+                KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), new KeycloakSessionTask() {
+
+                    @Override
+                    public void run(KeycloakSession session) {
+                        createRolesInBatch(session, roleContainer, rolePrefix, state.firstInThisBatch, state.countInThisBatch);
+                    }
+                });
+
+                // update state
+                state.firstInThisBatch = state.firstInThisBatch + state.countInThisBatch;
+                state.remaining = state.remaining - state.countInThisBatch;
+                state.countInThisBatch = Math.min(batchCount, state.remaining);
+            }
+
+            log.infof("Command finished. All roles from %s to %s created", rolePrefix + first, rolePrefix + (first + count - 1));
+        }
+
+        private void createRolesInBatch(KeycloakSession session, String roleContainer, String rolePrefix, int first, int count) {
+            RoleContainerModel container = getRoleContainer(session, roleContainer);
+
+            int last = first + count;
+            for (int counter = first; counter < last; counter++) {
+                String roleName = rolePrefix + counter;
+                RoleModel role = container.addRole(roleName);
+            }
+            log.infof("Roles from %s to %s created", rolePrefix + first, rolePrefix + (last - 1));
+        }
+
+        private RoleContainerModel getRoleContainer(KeycloakSession session, String roleContainer) {
+            String[] parts = roleContainer.split("/");
+            String realmName = parts[0];
+
+            RealmModel realm = session.realms().getRealmByName(realmName);
+            if (realm == null) {
+                log.errorf("Unknown realm: %s", realmName);
+                throw new HandledException();
+            }
+
+            if (parts.length == 1) {
+                return realm;
+            } else {
+                String clientId = parts[1];
+                ClientModel client = session.realms().getClientByClientId(clientId, realm);
+                if (client == null) {
+                    log.errorf("Unknown client: %s", clientId);
+                    throw new HandledException();
+                }
+
+                return client;
+            }
+        }
+
+        @Override
+        public String printUsage() {
+            return super.printUsage() + " <role-prefix> <role-container> <starting-role-offset> <total-count> <batch-size> . " +
+                    "\n'total-count' refers to total count of newly created roles. 'batch-size' refers to number of created roles in each transaction. 'starting-role-offset' refers to starting role offset." +
+                    "\nFor example if 'starting-role-offset' is 15 and total-count is 10 and role-prefix is 'test', it will create roles test15, test16, test17, ... , test24" +
+                    "\n'role-container' is either realm (then use just realmName like 'demo' or client (then use realm/clientId like 'demo/my-client' .\n" +
+                    "Example usage: " + super.printUsage() + " test demo 0 500 100";
+        }
+
+    }
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/TestCacheUtils.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/TestCacheUtils.java
new file mode 100644
index 0000000..9792f9d
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/TestCacheUtils.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.util.cli;
+
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientTemplateModel;
+import org.keycloak.models.FederatedIdentityModel;
+import org.keycloak.models.GroupModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleContainerModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.UserModel;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class TestCacheUtils {
+
+    public static void cacheRealmWithEverything(KeycloakSession session, String realmName) {
+        RealmModel realm  = session.realms().getRealmByName(realmName);
+
+        for (ClientModel client : realm.getClients()) {
+            realm.getClientById(client.getId());
+            realm.getClientByClientId(client.getClientId());
+
+            cacheRoles(session, realm, client);
+        }
+
+        cacheRoles(session, realm, realm);
+
+        for (GroupModel group : realm.getTopLevelGroups()) {
+            cacheGroupRecursive(realm, group);
+        }
+
+        for (ClientTemplateModel clientTemplate : realm.getClientTemplates()) {
+            realm.getClientTemplateById(clientTemplate.getId());
+        }
+
+        for (UserModel user : session.users().getUsers(realm)) {
+            session.users().getUserById(user.getId(), realm);
+            if (user.getEmail() != null) {
+                session.users().getUserByEmail(user.getEmail(), realm);
+            }
+            session.users().getUserByUsername(user.getUsername(), realm);
+
+            session.users().getConsents(realm, user.getId());
+
+            for (FederatedIdentityModel fedIdentity : session.users().getFederatedIdentities(user, realm)) {
+                session.users().getUserByFederatedIdentity(fedIdentity, realm);
+            }
+        }
+    }
+
+    private static void cacheRoles(KeycloakSession session, RealmModel realm, RoleContainerModel roleContainer) {
+        for (RoleModel role : roleContainer.getRoles()) {
+            realm.getRoleById(role.getId());
+            roleContainer.getRole(role.getName());
+            if (roleContainer instanceof RealmModel) {
+                session.realms().getRealmRole(realm, role.getName());
+            } else {
+                session.realms().getClientRole(realm, (ClientModel) roleContainer, role.getName());
+            }
+        }
+    }
+
+    private static void cacheGroupRecursive(RealmModel realm, GroupModel group) {
+        realm.getGroupById(group.getId());
+        for (GroupModel sub : group.getSubGroups()) {
+            cacheGroupRecursive(realm, sub);
+        }
+    }
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/TestsuiteCLI.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/TestsuiteCLI.java
index 8e9582b..9b2c17a 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/TestsuiteCLI.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/TestsuiteCLI.java
@@ -57,7 +57,11 @@ public class TestsuiteCLI {
             UserCommands.Remove.class,
             UserCommands.Count.class,
             UserCommands.GetUser.class,
-            SyncDummyFederationProviderCommand.class
+            SyncDummyFederationProviderCommand.class,
+            RoleCommands.CreateRoles.class,
+            CacheCommands.ListCachesCommand.class,
+            CacheCommands.GetCacheCommand.class,
+            CacheCommands.CacheRealmObjectsCommand.class
     };
 
     private final KeycloakSessionFactory sessionFactory;
diff --git a/testsuite/integration/src/test/resources/log4j.properties b/testsuite/integration/src/test/resources/log4j.properties
index f0ff6ac..2fa1d70 100755
--- a/testsuite/integration/src/test/resources/log4j.properties
+++ b/testsuite/integration/src/test/resources/log4j.properties
@@ -46,7 +46,8 @@ log4j.logger.org.keycloak.connections.jpa.updater.liquibase=${keycloak.liquibase
 # log4j.logger.org.keycloak.models.sessions.infinispan.initializer=trace
 
 # Enable to view cache activity
-# log4j.logger.org.keycloak.models.cache=trace
+#log4j.logger.org.keycloak.cluster.infinispan=trace
+#log4j.logger.org.keycloak.models.cache.infinispan=debug
 
 # Enable to view database updates
 # log4j.logger.org.keycloak.connections.mongo.updater.DefaultMongoUpdaterProvider=debug
diff --git a/testsuite/integration/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration/src/test/resources/META-INF/keycloak-server.json
index 06b4e52..3f4ddd1 100755
--- a/testsuite/integration/src/test/resources/META-INF/keycloak-server.json
+++ b/testsuite/integration/src/test/resources/META-INF/keycloak-server.json
@@ -97,7 +97,10 @@
         "default": {
             "clustered": "${keycloak.connectionsInfinispan.clustered:false}",
             "async": "${keycloak.connectionsInfinispan.async:false}",
-            "sessionsOwners": "${keycloak.connectionsInfinispan.sessionsOwners:2}"
+            "sessionsOwners": "${keycloak.connectionsInfinispan.sessionsOwners:2}",
+            "remoteStoreEnabled": "${keycloak.connectionsInfinispan.remoteStoreEnabled:false}",
+            "remoteStoreHost": "${keycloak.connectionsInfinispan.remoteStoreHost:localhost}",
+            "remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort:11222}"
         }
     },
 
diff --git a/testsuite/integration-arquillian/pom.xml b/testsuite/integration-arquillian/pom.xml
index c5adb34..dc30d10 100644
--- a/testsuite/integration-arquillian/pom.xml
+++ b/testsuite/integration-arquillian/pom.xml
@@ -139,12 +139,40 @@
             <properties>
                 <migrated.auth.server.version>${migration.project.version}</migrated.auth.server.version>
             </properties>
+            <build>
+                <pluginManagement>
+                    <plugins>
+                        <plugin>
+                            <artifactId>maven-surefire-plugin</artifactId>
+                            <configuration>
+                                <systemPropertyVariables>
+                                    <migrated.auth.server.version>${migrated.auth.server.version}</migrated.auth.server.version>
+                                </systemPropertyVariables>
+                            </configuration>
+                        </plugin>
+                    </plugins>
+                </pluginManagement>
+            </build>
         </profile>
         <profile>
             <id>test-product-migration</id>
             <properties>
                 <migrated.auth.server.version>${migration.product.version}</migrated.auth.server.version>
             </properties>
+            <build>
+                <pluginManagement>
+                    <plugins>
+                        <plugin>
+                            <artifactId>maven-surefire-plugin</artifactId>
+                            <configuration>
+                                <systemPropertyVariables>
+                                    <migrated.auth.server.version>${migrated.auth.server.version}</migrated.auth.server.version>
+                                </systemPropertyVariables>
+                            </configuration>
+                        </plugin>
+                    </plugins>
+                </pluginManagement>
+            </build>
         </profile>
     </profiles>
 
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingExportImportResource.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingExportImportResource.java
index a9ff31d..56c76ef 100644
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingExportImportResource.java
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingExportImportResource.java
@@ -36,7 +36,9 @@ import static org.keycloak.exportimport.ExportImportConfig.DIR;
 import static org.keycloak.exportimport.ExportImportConfig.FILE;
 import static org.keycloak.exportimport.ExportImportConfig.PROVIDER;
 import static org.keycloak.exportimport.ExportImportConfig.REALM_NAME;
+import static org.keycloak.exportimport.ExportImportConfig.STRATEGY;
 import static org.keycloak.exportimport.ExportImportConfig.USERS_PER_FILE;
+import org.keycloak.exportimport.Strategy;
 
 /**
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -98,6 +100,13 @@ public class TestingExportImportResource {
     }
 
     @PUT
+    @Path("/set-import-strategy")
+    @Consumes(MediaType.APPLICATION_JSON)
+    public void setStrategy(@QueryParam("importStrategy") Strategy strategy) {
+        System.setProperty(STRATEGY, strategy.name());
+    }
+
+    @PUT
     @Path("/export-import-provider")
     @Consumes(MediaType.APPLICATION_JSON)
     public void setProvider(@QueryParam("exportImportProvider") String exportImportProvider) {
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java
index 7dd6b24..81c5a53 100644
--- a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java
@@ -34,6 +34,8 @@ import java.util.Set;
 @Transaction
 public class AlbumService {
 
+    private static volatile long nextId = 0;
+
     public static final String SCOPE_ALBUM_VIEW = "urn:photoz.com:scopes:album:view";
     public static final String SCOPE_ALBUM_CREATE = "urn:photoz.com:scopes:album:create";
     public static final String SCOPE_ALBUM_DELETE = "urn:photoz.com:scopes:album:delete";
@@ -53,6 +55,8 @@ public class AlbumService {
     @POST
     @Consumes("application/json")
     public Response create(Album newAlbum) {
+        newAlbum.setId(++nextId);
+
         Principal userPrincipal = request.getUserPrincipal();
 
         newAlbum.setUserId(userPrincipal.getName());
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/entity/Album.java b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/entity/Album.java
index 978bdea..cc8bea2 100644
--- a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/entity/Album.java
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/entity/Album.java
@@ -23,6 +23,7 @@ import javax.persistence.FetchType;
 import javax.persistence.GeneratedValue;
 import javax.persistence.Id;
 import javax.persistence.OneToMany;
+import javax.persistence.GenerationType;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -33,7 +34,6 @@ import java.util.List;
 public class Album {
 
     @Id
-    @GeneratedValue
     private Long id;
 
     @Column(nullable = false)
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/migration/MigrationTestExecutionDecider.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/migration/MigrationTestExecutionDecider.java
index 3b457ec..b8aa42c 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/migration/MigrationTestExecutionDecider.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/migration/MigrationTestExecutionDecider.java
@@ -20,6 +20,7 @@ import org.jboss.arquillian.test.spi.execution.ExecutionDecision;
 import org.jboss.arquillian.test.spi.execution.TestExecutionDecider;
 
 import java.lang.reflect.Method;
+import org.jboss.logging.Logger;
 
 /**
  * @author <a href="mailto:vramik@redhat.com">Vlastislav Ramik</a>
@@ -27,6 +28,7 @@ import java.lang.reflect.Method;
  */
 public class MigrationTestExecutionDecider implements TestExecutionDecider {
 
+    private final Logger log = Logger.getLogger(MigrationTestExecutionDecider.class);
     private static final String MIGRATED_AUTH_SERVER_VERSION_PROPERTY = "migrated.auth.server.version";
 
     @Override
@@ -35,8 +37,10 @@ public class MigrationTestExecutionDecider implements TestExecutionDecider {
         String migratedAuthServerVersion = System.getProperty(MIGRATED_AUTH_SERVER_VERSION_PROPERTY);
         boolean migrationTest = migratedAuthServerVersion != null;
         Migration migrationAnnotation = method.getAnnotation(Migration.class);
-
-        if (migrationTest && migrationAnnotation != null) {
+        
+         if (migrationTest && migrationAnnotation != null) {
+            log.info("migration from version: " + migratedAuthServerVersion);
+            
             String versionFrom = migrationAnnotation.versionFrom();
 
             if (migratedAuthServerVersion.contains(versionFrom)) {
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingExportImportResource.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingExportImportResource.java
index 27fa360..0c2f106 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingExportImportResource.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingExportImportResource.java
@@ -25,6 +25,7 @@ import javax.ws.rs.Produces;
 import javax.ws.rs.QueryParam;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
+import org.keycloak.exportimport.Strategy;
 
 /**
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -65,6 +66,11 @@ public interface TestingExportImportResource {
     public String setDir(@QueryParam("dir") String dir);
 
     @PUT
+    @Path("/set-import-strategy")
+    @Consumes(MediaType.APPLICATION_JSON)
+    public void setStrategy(@QueryParam("importStrategy") Strategy strategy);
+
+    @PUT
     @Path("/export-import-provider")
     @Consumes(MediaType.APPLICATION_JSON)
     public void setProvider(@QueryParam("exportImportProvider") String exportImportProvider);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java
index 89d5c5b..87950ab 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java
@@ -257,39 +257,22 @@ public abstract class AbstractKeycloakTest {
         adminClient.realms().create(realm);
     }
 
-    public void removeRealm(RealmRepresentation realm) {
+    public void removeRealm(String realmName) {
+        log.info("removing realm: " + realmName);
         try {
-            adminClient.realms().realm(realm.getRealm()).remove();
+            adminClient.realms().realm(realmName).remove();
         } catch (NotFoundException e) {
         }
     }
+
+    public void removeRealm(RealmRepresentation realm) {
+        removeRealm(realm.getRealm());
+    }
     
     public RealmsResource realmsResouce() {
         return adminClient.realms();
     }
 
-    public void createRealm(String realm) {
-        try {
-            RealmResource realmResource = adminClient.realms().realm(realm);
-            // Throws NotFoundException in case the realm does not exist! Ugly but there
-            // does not seem to be a way to this just by asking.
-            RealmRepresentation realmRepresentation = realmResource.toRepresentation();
-        } catch (NotFoundException ex) {
-            RealmRepresentation realmRepresentation = new RealmRepresentation();
-            realmRepresentation.setRealm(realm);
-            realmRepresentation.setEnabled(true);
-            realmRepresentation.setRegistrationAllowed(true);
-            adminClient.realms().create(realmRepresentation);
-
-//            List<RequiredActionProviderRepresentation> requiredActions = adminClient.realm(realm).flows().getRequiredActions();
-//            for (RequiredActionProviderRepresentation a : requiredActions) {
-//                a.setEnabled(false);
-//                a.setDefaultAction(false);
-//                adminClient.realm(realm).flows().updateRequiredAction(a.getAlias(), a);
-//            }
-        }
-    }
-
     /**
      * Creates a user in the given realm and returns its ID.
      * @param realm Realm name
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java
index 65a2e9b..90bf62b 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java
@@ -58,6 +58,8 @@ import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 
+import static org.hamcrest.Matchers.*;
+
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
  * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
@@ -756,7 +758,7 @@ public class AccountTest extends TestRealmKeycloakTest {
         Assert.assertTrue(applicationsPage.isCurrent());
 
         Map<String, AccountApplicationsPage.AppEntry> apps = applicationsPage.getApplications();
-        Assert.assertEquals(4, apps.size());
+        Assert.assertThat(apps.keySet(), containsInAnyOrder("Account", "test-app", "test-app-scope", "third-party", "test-app-authz"));
 
         AccountApplicationsPage.AppEntry accountEntry = apps.get("Account");
         Assert.assertEquals(2, accountEntry.getRolesAvailable().size());
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/custom/CustomAuthFlowOTPTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/custom/CustomAuthFlowOTPTest.java
index 0230842..e492672 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/custom/CustomAuthFlowOTPTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/custom/CustomAuthFlowOTPTest.java
@@ -17,7 +17,6 @@
 package org.keycloak.testsuite.account.custom;
 
 import org.jboss.arquillian.graphene.page.Page;
-import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 import org.keycloak.models.AuthenticationExecutionModel.Requirement;
@@ -28,6 +27,7 @@ import org.keycloak.representations.idm.RealmRepresentation;
 import org.keycloak.representations.idm.RoleRepresentation;
 import org.keycloak.testsuite.admin.Users;
 import org.keycloak.testsuite.auth.page.login.OneTimeCode;
+import org.keycloak.testsuite.pages.LoginConfigTotpPage;
 
 import javax.ws.rs.core.Response;
 import java.util.ArrayList;
@@ -35,6 +35,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.keycloak.authentication.authenticators.browser.ConditionalOtpFormAuthenticator.DEFAULT_OTP_OUTCOME;
 import static org.keycloak.authentication.authenticators.browser.ConditionalOtpFormAuthenticator.FORCE;
@@ -58,6 +59,9 @@ public class CustomAuthFlowOTPTest extends AbstractCustomAccountManagementTest {
     
     @Page
     private OneTimeCode testLoginOneTimeCodePage;
+
+    @Page
+    private LoginConfigTotpPage loginConfigTotpPage;
     
     @Override
     public void setDefaultPageUriParameters() {
@@ -69,12 +73,17 @@ public class CustomAuthFlowOTPTest extends AbstractCustomAccountManagementTest {
     @Override
     public void beforeTest() {
         super.beforeTest();
+    }
+
+    private void configureRequiredActions() {
         //set configure TOTP as required action to test user
         List<String> requiredActions = new ArrayList<>();
         requiredActions.add(CONFIGURE_TOTP.name());
         testUser.setRequiredActions(requiredActions);
         testRealmResource().users().get(testUser.getId()).update(testUser);
-        
+    }
+
+    private void configureOTP() {
         //configure OTP for test user
         testRealmAccountManagementPage.navigateTo();
         testRealmLoginPage.form().login(testUser);
@@ -83,7 +92,7 @@ public class CustomAuthFlowOTPTest extends AbstractCustomAccountManagementTest {
         testRealmLoginPage.form().totpForm().setTotp(totp.generateTOTP(totpSecret));
         testRealmLoginPage.form().totpForm().submit();
         testRealmAccountManagementPage.signOut();
-        
+
         //verify that user has OTP configured
         testUser = testRealmResource().users().get(testUser.getId()).toRepresentation();
         Users.setPasswordFor(testUser, PASSWORD);
@@ -92,40 +101,45 @@ public class CustomAuthFlowOTPTest extends AbstractCustomAccountManagementTest {
 
     @Test
     public void requireOTPTest() {
-        
+
         updateRequirement("browser", "auth-otp-form", Requirement.REQUIRED);
-        
         testRealmAccountManagementPage.navigateTo();
         testRealmLoginPage.form().login(testUser);
+        assertTrue(loginConfigTotpPage.isCurrent());
+
+        configureOTP();
+        testRealmLoginPage.form().login(testUser);
         testRealmLoginPage.form().totpForm().waitForTotpInputFieldPresent();
-        
+
         //verify that the page is login page, not totp setup
         assertCurrentUrlStartsWith(testLoginOneTimeCodePage);
     }
-    
+
     @Test
     public void conditionalOTPNoDefault() {
+        configureRequiredActions();
+        configureOTP();
         //prepare config - no configuration specified
         Map<String, String> config = new HashMap<>();
         setConditionalOTPForm(config);
-        
+
         //test OTP is required
         testRealmAccountManagementPage.navigateTo();
         testRealmLoginPage.form().login(testUser);
         testRealmLoginPage.form().totpForm().waitForTotpInputFieldPresent();
-        
+
         //verify that the page is login page, not totp setup
         assertCurrentUrlStartsWith(testLoginOneTimeCodePage);
     }
-    
+
     @Test
     public void conditionalOTPDefaultSkip() {
         //prepare config - default skip
         Map<String, String> config = new HashMap<>();
         config.put(DEFAULT_OTP_OUTCOME, SKIP);
-        
+
         setConditionalOTPForm(config);
-        
+
         //test OTP is skipped
         testRealmAccountManagementPage.navigateTo();
         testRealmLoginPage.form().login(testUser);
@@ -134,6 +148,7 @@ public class CustomAuthFlowOTPTest extends AbstractCustomAccountManagementTest {
     
     @Test
     public void conditionalOTPDefaultForce() {
+
         //prepare config - default force
         Map<String, String> config = new HashMap<>();
         config.put(DEFAULT_OTP_OUTCOME, FORCE);
@@ -143,8 +158,12 @@ public class CustomAuthFlowOTPTest extends AbstractCustomAccountManagementTest {
         //test OTP is forced
         testRealmAccountManagementPage.navigateTo();
         testRealmLoginPage.form().login(testUser);
+        assertTrue(loginConfigTotpPage.isCurrent());
+
+        configureOTP();
+        testRealmLoginPage.form().login(testUser);
         testRealmLoginPage.form().totpForm().waitForTotpInputFieldPresent();
-        
+
         //verify that the page is login page, not totp setup
         assertCurrentUrlStartsWith(testLoginOneTimeCodePage);
     }
@@ -155,48 +174,54 @@ public class CustomAuthFlowOTPTest extends AbstractCustomAccountManagementTest {
         Map<String, String> config = new HashMap<>();
         config.put(OTP_CONTROL_USER_ATTRIBUTE, "userSkipAttribute");
         config.put(DEFAULT_OTP_OUTCOME, FORCE);
-        
+
         setConditionalOTPForm(config);
 
         //add skip user attribute to user
         testUser.singleAttribute("userSkipAttribute", "skip");
         testRealmResource().users().get(testUser.getId()).update(testUser);
-        
+
         //test OTP is skipped
         testRealmAccountManagementPage.navigateTo();
         testRealmLoginPage.form().login(testUser);
+
         assertCurrentUrlStartsWith(testRealmAccountManagementPage);
     }
-    
+
     @Test
     public void conditionalOTPUserAttributeForce() {
+
         //prepare config - user attribute, default to skip
         Map<String, String> config = new HashMap<>();
         config.put(OTP_CONTROL_USER_ATTRIBUTE, "userSkipAttribute");
         config.put(DEFAULT_OTP_OUTCOME, SKIP);
-        
+
         setConditionalOTPForm(config);
 
         //add force user attribute to user
         testUser.singleAttribute("userSkipAttribute", "force");
         testRealmResource().users().get(testUser.getId()).update(testUser);
-        
+
         //test OTP is required
         testRealmAccountManagementPage.navigateTo();
         testRealmLoginPage.form().login(testUser);
+        assertTrue(loginConfigTotpPage.isCurrent());
+
+        configureOTP();
+        testRealmLoginPage.form().login(testUser);
         testRealmLoginPage.form().totpForm().waitForTotpInputFieldPresent();
-        
+
         //verify that the page is login page, not totp setup
         assertCurrentUrlStartsWith(testLoginOneTimeCodePage);
     }
-    
+
     @Test
     public void conditionalOTPRoleSkip() {
         //prepare config - role, default to force
         Map<String, String> config = new HashMap<>();
         config.put(SKIP_OTP_ROLE, "otp_role");
         config.put(DEFAULT_OTP_OUTCOME, FORCE);
-        
+
         setConditionalOTPForm(config);
 
         //create role
@@ -208,20 +233,20 @@ public class CustomAuthFlowOTPTest extends AbstractCustomAccountManagementTest {
         List<RoleRepresentation> realmRoles = new ArrayList<>();
         realmRoles.add(role);
         testRealmResource().users().get(testUser.getId()).roles().realmLevel().add(realmRoles);
-        
+
         //test OTP is skipped
         testRealmAccountManagementPage.navigateTo();
         testRealmLoginPage.form().login(testUser);
         assertCurrentUrlStartsWith(testRealmAccountManagementPage);
     }
-    
+
     @Test
     public void conditionalOTPRoleForce() {
         //prepare config - role, default to skip
         Map<String, String> config = new HashMap<>();
         config.put(FORCE_OTP_ROLE, "otp_role");
         config.put(DEFAULT_OTP_OUTCOME, SKIP);
-        
+
         setConditionalOTPForm(config);
 
         //create role
@@ -233,16 +258,21 @@ public class CustomAuthFlowOTPTest extends AbstractCustomAccountManagementTest {
         List<RoleRepresentation> realmRoles = new ArrayList<>();
         realmRoles.add(role);
         testRealmResource().users().get(testUser.getId()).roles().realmLevel().add(realmRoles);
-        
+
         //test OTP is required
         testRealmAccountManagementPage.navigateTo();
         testRealmLoginPage.form().login(testUser);
+
+        assertTrue(loginConfigTotpPage.isCurrent());
+
+        configureOTP();
+        testRealmLoginPage.form().login(testUser);
         testRealmLoginPage.form().totpForm().waitForTotpInputFieldPresent();
-        
+
         //verify that the page is login page, not totp setup
         assertCurrentUrlStartsWith(testLoginOneTimeCodePage);
     }
-    
+
     @Test
     public void conditionalOTPRequestHeaderSkip() {
         //prepare config - request header skip, default to force
@@ -250,7 +280,7 @@ public class CustomAuthFlowOTPTest extends AbstractCustomAccountManagementTest {
         String port = System.getProperty("auth.server.http.port", "8180");
         config.put(SKIP_OTP_FOR_HTTP_HEADER, "Host: localhost:" + port);
         config.put(DEFAULT_OTP_OUTCOME, FORCE);
-        
+
         setConditionalOTPForm(config);
 
         //test OTP is skipped
@@ -258,7 +288,7 @@ public class CustomAuthFlowOTPTest extends AbstractCustomAccountManagementTest {
         testRealmLoginPage.form().login(testUser);
         assertCurrentUrlStartsWith(testRealmAccountManagementPage);
     }
-    
+
     @Test
     public void conditionalOTPRequestHeaderForce() {
         //prepare config - equest header force, default to skip
@@ -266,18 +296,22 @@ public class CustomAuthFlowOTPTest extends AbstractCustomAccountManagementTest {
         String port = System.getProperty("auth.server.http.port", "8180");
         config.put(FORCE_OTP_FOR_HTTP_HEADER, "Host: localhost:" + port);
         config.put(DEFAULT_OTP_OUTCOME, SKIP);
-        
+
         setConditionalOTPForm(config);
 
         //test OTP is required
         testRealmAccountManagementPage.navigateTo();
         testRealmLoginPage.form().login(testUser);
+        assertEquals(driver.getTitle(), "Mobile Authenticator Setup");
+
+        configureOTP();
+        testRealmLoginPage.form().login(testUser);
         testRealmLoginPage.form().totpForm().waitForTotpInputFieldPresent();
-        
+
         //verify that the page is login page, not totp setup
         assertCurrentUrlStartsWith(testLoginOneTimeCodePage);
     }
-    
+
     private void setConditionalOTPForm(Map<String, String> config) {
         String flowAlias = "ConditionalOTPFlow";
         String provider = "auth-conditional-otp-form";
@@ -291,7 +325,7 @@ public class CustomAuthFlowOTPTest extends AbstractCustomAccountManagementTest {
         flow.setBuiltIn(false);
         
         Response response = getAuthMgmtResource().createFlow(flow);
-        Assert.assertEquals(flowAlias + " create success", 201, response.getStatus());
+        assertEquals(flowAlias + " create success", 201, response.getStatus());
         response.close();
         
         //add execution - username-password form
@@ -322,10 +356,10 @@ public class CustomAuthFlowOTPTest extends AbstractCustomAccountManagementTest {
         AuthenticatorConfigRepresentation authConfig = new AuthenticatorConfigRepresentation();
         authConfig.setAlias("Config alias");
         authConfig.setConfig(config);
-        
+
         //add auth config to the execution
         response = getAuthMgmtResource().newExecutionConfig(executionId, authConfig);
-        Assert.assertEquals("new execution success", 201, response.getStatus());
+        assertEquals("new execution success", 201, response.getStatus());
         response.close();
     }
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/FlowTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/FlowTest.java
index 90f8874..bce9117 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/FlowTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/FlowTest.java
@@ -37,6 +37,33 @@ import java.util.Map;
  */
 public class FlowTest extends AbstractAuthenticationTest {
 
+    // KEYCLOAK-3681: Delete top flow doesn't delete all subflows
+    @Test
+    public void testRemoveSubflows() {
+        authMgmtResource.createFlow(newFlow("Foo", "Foo flow", "generic", true, false));
+        addFlowToParent("Foo", "child");
+        addFlowToParent("child", "grandchild");
+        
+        List<AuthenticationFlowRepresentation> flows = authMgmtResource.getFlows();
+        AuthenticationFlowRepresentation found = findFlowByAlias("Foo", flows);
+        authMgmtResource.deleteFlow(found.getId());
+        
+        authMgmtResource.createFlow(newFlow("Foo", "Foo flow", "generic", true, false));
+        addFlowToParent("Foo", "child");
+        
+        // Under the old code, this would throw an error because "grandchild"
+        // was left in the database
+        addFlowToParent("child", "grandchild");
+    }
+    
+    private void addFlowToParent(String parentAlias, String childAlias) {
+        Map<String, String> data = new HashMap<>();
+        data.put("alias", childAlias);
+        data.put("type", "generic");
+        data.put("description", childAlias + " flow");
+        authMgmtResource.addExecutionFlow(parentAlias, data);
+    }
+    
     @Test
     public void testAddRemoveFlow() {
 
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java
index 7e61f51..1f0274e 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java
@@ -438,7 +438,7 @@ public class GroupTest extends AbstractGroupTest {
 
         // List realm roles
         assertNames(roles.realmLevel().listAll(), "realm-role", "realm-composite");
-        assertNames(roles.realmLevel().listAvailable(), "admin", "offline_access", Constants.AUTHZ_UMA_AUTHORIZATION, "user", "customer-user-premium");
+        assertNames(roles.realmLevel().listAvailable(), "admin", "offline_access", Constants.AUTHZ_UMA_AUTHORIZATION, "user", "customer-user-premium", "realm-composite-role", "sample-realm-role");
         assertNames(roles.realmLevel().listEffective(), "realm-role", "realm-composite", "realm-child");
 
         // List client roles
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java
index 81af8c6..545fbff 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java
@@ -817,7 +817,7 @@ public class UserTest extends AbstractAdminTest {
 
         // List realm roles
         assertNames(roles.realmLevel().listAll(), "realm-role", "realm-composite", "user", "offline_access", Constants.AUTHZ_UMA_AUTHORIZATION);
-        assertNames(roles.realmLevel().listAvailable(), "admin", "customer-user-premium");
+        assertNames(roles.realmLevel().listAvailable(), "admin", "customer-user-premium", "realm-composite-role", "sample-realm-role");
         assertNames(roles.realmLevel().listEffective(), "realm-role", "realm-composite", "realm-child", "user", "offline_access", Constants.AUTHZ_UMA_AUTHORIZATION);
 
         // List client roles
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/SAMLClientRegistrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/SAMLClientRegistrationTest.java
index c3e60a2..6efbe9a 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/SAMLClientRegistrationTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/SAMLClientRegistrationTest.java
@@ -28,8 +28,8 @@ import org.keycloak.representations.idm.ClientRepresentation;
 
 import java.io.IOException;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+import static org.hamcrest.Matchers.*;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -49,10 +49,14 @@ public class SAMLClientRegistrationTest extends AbstractClientRegistrationTest {
         String entityDescriptor = IOUtils.toString(getClass().getResourceAsStream("/clientreg-test/saml-entity-descriptor.xml"));
         ClientRepresentation response = reg.saml().create(entityDescriptor);
 
-        assertNotNull(response.getRegistrationAccessToken());
-        assertEquals("loadbalancer-9.siroe.com", response.getClientId());
-        assertEquals(1, response.getRedirectUris().size());
-        assertEquals("https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp", response.getRedirectUris().get(0));
+        assertThat(response.getRegistrationAccessToken(), notNullValue());
+        assertThat(response.getClientId(), is("loadbalancer-9.siroe.com"));
+        assertThat(response.getRedirectUris(), containsInAnyOrder(
+          "https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp/post",
+          "https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp/soap",
+          "https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp/paos",
+          "https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp/redirect"
+        ));  // No redirect URI for ARTIFACT binding which is unsupported
     }
 
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java
index 90dda59..2abf044 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java
@@ -141,11 +141,7 @@ public class ExportImportTest extends AbstractExportImportTest {
 
         ExportImportUtil.assertDataImportedInRealm(adminClient, testingClient, testRealmRealm.toRepresentation());
     }
-
-    private void removeRealm(String realmName) {
-        adminClient.realm(realmName).remove();
-    }
-
+    
     private void testFullExportImport() throws LifecycleException {
         testingClient.testing().exportImport().setAction(ExportImportConfig.ACTION_EXPORT);
         testingClient.testing().exportImport().setRealmName("");
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/LegacyImportTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/LegacyImportTest.java
index 45808a3..e7ec574 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/LegacyImportTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/LegacyImportTest.java
@@ -20,7 +20,6 @@ package org.keycloak.testsuite.exportimport;
 import org.jboss.arquillian.container.spi.client.container.LifecycleException;
 import org.junit.After;
 import org.junit.Assert;
-import org.junit.Ignore;
 import org.junit.Test;
 import org.keycloak.Config;
 import org.keycloak.admin.client.resource.ClientResource;
@@ -39,6 +38,11 @@ import java.net.URL;
 import java.util.Collection;
 import java.util.List;
 import java.util.Set;
+import static org.junit.Assert.assertNotNull;
+import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.exportimport.Strategy;
+import static org.keycloak.testsuite.Assert.assertNames;
+import static org.keycloak.testsuite.migration.MigrationTest.MIGRATION;
 
 /**
  * Test importing JSON files exported from previous adminClient versions
@@ -57,46 +61,82 @@ public class LegacyImportTest extends AbstractExportImportTest {
     public void addTestRealms(List<RealmRepresentation> testRealms) {
     }
 
-
-    @Ignore // TODO: Restart and set system properties doesn't work on wildfly ATM. Figure and re-enable
     @Test
-    public void importFrom11() throws LifecycleException {
-        // Setup system properties for import ( TODO: Set properly with external-container )
-        ExportImportConfig.setProvider(SingleFileExportProviderFactory.PROVIDER_ID);
-        URL url = LegacyImportTest.class.getResource("/exportimport-test/kc11-exported-realm.json");
-        String targetFilePath = new File(url.getFile()).getAbsolutePath();
-        ExportImportConfig.setFile(targetFilePath);
-        ExportImportConfig.setAction(ExportImportConfig.ACTION_IMPORT);
-
-        // Restart to enforce full import
-        restartServer();
+    public void importPreviousProject() throws Exception {
 
+        String projectVersion = System.getProperty("migration.project.version");
+        assertNotNull(projectVersion);
+        
+        testLegacyImport(projectVersion);
+    }
+    
+    @Test
+    public void importPreviousProduct() throws Exception {
 
-        // Assert "locale" mapper available in security-admin-console client for both master and foo11 realm
-        ClientResource foo11AdminConsoleClient = adminClient.realm("foo11").clients().get("a9ca4217-74a8-4658-92c8-c2f9ed48a474");
-        assertLocaleMapperPresent(foo11AdminConsoleClient);
-
-        ClientResource masterAdminConsoleClient = adminClient.realm(Config.getAdminRealm()).clients().get("22ed594d-8c21-43f0-a080-c8879a411f94");
-        assertLocaleMapperPresent(masterAdminConsoleClient);
-
-
-        // Assert "realm-management" role correctly set and contains all admin roles.
-        ClientResource foo11RealmManagementClient = adminClient.realm("foo11").clients().get("c7a9cf59-feeb-44a4-a467-e008e157efa2");
-        List<RoleRepresentation> roles = foo11RealmManagementClient.roles().list();
-        assertRolesAvailable(roles);
-
-        // Assert all admin roles are also available as composites of "realm-admin"
-        Set<RoleRepresentation> realmAdminComposites = foo11RealmManagementClient.roles().get(AdminRoles.REALM_ADMIN).getRoleComposites();
-        assertRolesAvailable(realmAdminComposites);
+        String productVersion = System.getProperty("migration.product.version");
+        assertNotNull(productVersion);
+        
+        testLegacyImport(productVersion);
+    }
 
-        // Assert "foo11-master" client correctly set and contains all admin roles.
-        ClientResource foo11MasterAdminClient = adminClient.realm(Config.getAdminRealm()).clients().get("c9c3bd5f-b69d-4640-8b27-45d4f3866a36");
-        roles = foo11MasterAdminClient.roles().list();
-        assertRolesAvailable(roles);
+    private void testLegacyImport(String version) {
+        String file = "/migration-test/migration-realm-" + version + ".json";
+        
+        URL url = LegacyImportTest.class.getResource(file);
+        String targetFilePath = new File(url.getFile()).getAbsolutePath();
+        testingClient.testing().exportImport().setFile(targetFilePath);
+        testingClient.testing().exportImport().setProvider(SingleFileExportProviderFactory.PROVIDER_ID);
+        testingClient.testing().exportImport().setAction(ExportImportConfig.ACTION_IMPORT);
+        testingClient.testing().exportImport().setRealmName(MIGRATION);
+        testingClient.testing().exportImport().setStrategy(Strategy.IGNORE_EXISTING);
+        
+        try {
+            testingClient.testing().exportImport().runImport();
+
+            RealmResource imported = adminClient.realm(MIGRATION);
+
+            assertNames(imported.roles().list(), "offline_access", "uma_authorization", "migration-test-realm-role");
+            assertNames(imported.clients().findAll(), "account", "admin-cli", "broker", "migration-test-client", "realm-management", "security-admin-console");
+            String id = imported.clients().findByClientId("migration-test-client").get(0).getId();
+            assertNames(imported.clients().get(id).roles().list(), "migration-test-client-role");
+            assertNames(imported.users().search("", 0, 5), "migration-test-user");
+            assertNames(imported.groups().groups(), "migration-test-group");
+        } finally {
+            removeRealm(MIGRATION);
+        }
+    }
 
-        // Assert all admin roles are also available as composites of "admin" role
-        Set<RoleRepresentation> masterAdminComposites = adminClient.realm(Config.getAdminRealm()).roles().get(AdminRoles.ADMIN).getRoleComposites();
-        assertRolesAvailable(masterAdminComposites);
+    //KEYCLOAK-1982
+    @Test
+    public void importFrom11() throws LifecycleException {
+        URL url = LegacyImportTest.class.getResource("/exportimport-test/kc11-exported-realm.json");
+        String targetFilePath = new File(url.getFile()).getAbsolutePath();
+        testingClient.testing().exportImport().setFile(targetFilePath);
+        testingClient.testing().exportImport().setProvider(SingleFileExportProviderFactory.PROVIDER_ID);
+        testingClient.testing().exportImport().setAction(ExportImportConfig.ACTION_IMPORT);
+
+        try {
+            testingClient.testing().exportImport().runImport();
+
+            // Assert "locale" mapper available in security-admin-console client
+            ClientResource foo11AdminConsoleClient = adminClient.realm("foo11").clients().get("a9ca4217-74a8-4658-92c8-c2f9ed48a474");
+            assertLocaleMapperPresent(foo11AdminConsoleClient);
+
+            // Assert "realm-management" role correctly set and contains all admin roles.
+            ClientResource foo11RealmManagementClient = adminClient.realm("foo11").clients().get("c7a9cf59-feeb-44a4-a467-e008e157efa2");
+            List<RoleRepresentation> roles = foo11RealmManagementClient.roles().list();
+            assertRolesAvailable(roles);
+
+            // Assert all admin roles are also available as composites of "realm-admin"
+            Set<RoleRepresentation> realmAdminComposites = foo11RealmManagementClient.roles().get(AdminRoles.REALM_ADMIN).getRoleComposites();
+            assertRolesAvailable(realmAdminComposites);
+
+            // Assert all admin roles are also available as composites of "admin" role
+            Set<RoleRepresentation> masterAdminComposites = adminClient.realm(Config.getAdminRealm()).roles().get(AdminRoles.ADMIN).getRoleComposites();
+            assertRolesAvailable(masterAdminComposites);
+        } finally {
+            removeRealm("foo11");
+        }
     }
 
 
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java
index 72d0c70..fe789ac 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java
@@ -52,7 +52,7 @@ import static org.keycloak.testsuite.auth.page.AuthRealm.MASTER;
  */
 public class MigrationTest extends AbstractKeycloakTest {
 
-    private final String MIGRATION = "Migration";
+    public static final String MIGRATION = "Migration";
     private RealmResource migrationRealm;
     private RealmResource masterRealm;
         
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LoginStatusIframeEndpointTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LoginStatusIframeEndpointTest.java
index 4bb437c..7a01e4e 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LoginStatusIframeEndpointTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LoginStatusIframeEndpointTest.java
@@ -31,12 +31,15 @@ import org.apache.http.impl.client.CloseableHttpClient;
 import org.apache.http.impl.client.HttpClients;
 import org.apache.http.message.BasicNameValuePair;
 import org.junit.Test;
+import org.keycloak.admin.client.resource.ClientResource;
 import org.keycloak.models.Constants;
+import org.keycloak.representations.idm.ClientRepresentation;
 import org.keycloak.representations.idm.RealmRepresentation;
 import org.keycloak.testsuite.AbstractKeycloakTest;
 
 import java.io.IOException;
 import java.net.URLEncoder;
+import java.util.Collections;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.regex.Matcher;
@@ -159,6 +162,31 @@ public class LoginStatusIframeEndpointTest extends AbstractKeycloakTest {
         }
     }
 
+    @Test
+    public void checkIframeWildcardOrigin() throws IOException {
+        String id = adminClient.realm("master").clients().findByClientId(Constants.ADMIN_CONSOLE_CLIENT_ID).get(0).getId();
+        ClientResource master = adminClient.realm("master").clients().get(id);
+        ClientRepresentation rep = master.toRepresentation();
+        List<String> org = rep.getWebOrigins();
+        CloseableHttpClient client = HttpClients.createDefault();
+        try {
+            rep.setWebOrigins(Collections.singletonList("*"));
+            master.update(rep);
+
+            HttpGet get = new HttpGet(suiteContext.getAuthServerInfo().getContextRoot() + "/auth/realms/master/protocol/openid-connect/login-status-iframe.html/init?"
+                    + "client_id=" + Constants.ADMIN_CONSOLE_CLIENT_ID
+                    + "&origin=" + "http://anything"
+            );
+            CloseableHttpResponse response = client.execute(get);
+            assertEquals(204, response.getStatusLine().getStatusCode());
+            response.close();
+        } finally {
+            rep.setWebOrigins(org);
+            master.update(rep);
+            client.close();
+        }
+    }
+
     @Override
     public void addTestRealms(List<RealmRepresentation> testRealms) {
     }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OIDCProtocolMappersTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OIDCProtocolMappersTest.java
index d317af7..ac86d20 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OIDCProtocolMappersTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OIDCProtocolMappersTest.java
@@ -43,10 +43,8 @@ import org.keycloak.testsuite.util.ClientManager;
 import org.keycloak.testsuite.util.OAuthClient;
 import org.keycloak.testsuite.util.ProtocolMapperUtil;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
 import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
 import static org.keycloak.testsuite.admin.ApiUtil.findClientByClientId;
 import static org.keycloak.testsuite.admin.ApiUtil.findClientResourceByClientId;
@@ -222,11 +220,152 @@ public class OIDCProtocolMappersTest extends AbstractKeycloakTest {
 
         // Verify attribute is filled
         Map<String, Object> roleMappings = (Map<String, Object>)idToken.getOtherClaims().get("roles-custom");
-        Assert.assertEquals(2, roleMappings.size());
+        Assert.assertThat(roleMappings.keySet(), containsInAnyOrder("realm", "test-app"));
         String realmRoleMappings = (String) roleMappings.get("realm");
         String testAppMappings = (String) roleMappings.get("test-app");
-        Assert.assertTrue(realmRoleMappings.contains("pref.user"));
-        Assert.assertEquals("[customer-user]", testAppMappings);
+        assertRoles(realmRoleMappings,
+          "pref.user",                      // from direct assignment in user definition
+          "pref.offline_access"             // from direct assignment in user definition
+        );
+        assertRoles(testAppMappings,
+          "customer-user"                   // from direct assignment in user definition
+        );
+    }
+
+
+    @Test
+    public void testUserGroupRoleToAttributeMappers() throws Exception {
+        // Add mapper for realm roles
+        String clientId = "test-app";
+        ProtocolMapperRepresentation realmMapper = ProtocolMapperUtil.createUserRealmRoleMappingMapper("pref.", "Realm roles mapper", "roles-custom.realm", true, true);
+        ProtocolMapperRepresentation clientMapper = ProtocolMapperUtil.createUserClientRoleMappingMapper(clientId, "ta.", "Client roles mapper", "roles-custom.test-app", true, true);
+
+        ProtocolMappersResource protocolMappers = ApiUtil.findClientResourceByClientId(adminClient.realm("test"), clientId).getProtocolMappers();
+        protocolMappers.createMapper(Arrays.asList(realmMapper, clientMapper));
+
+        // Login user
+        OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("password", "rich.roles@redhat.com", "password");
+        IDToken idToken = oauth.verifyIDToken(response.getIdToken());
+
+        // Verify attribute is filled
+        Map<String, Object> roleMappings = (Map<String, Object>)idToken.getOtherClaims().get("roles-custom");
+        Assert.assertThat(roleMappings.keySet(), containsInAnyOrder("realm", clientId));
+        String realmRoleMappings = (String) roleMappings.get("realm");
+        String testAppMappings = (String) roleMappings.get(clientId);
+        assertRoles(realmRoleMappings,
+          "pref.admin",                     // from direct assignment to /roleRichGroup/level2group
+          "pref.user",                      // from parent group of /roleRichGroup/level2group, i.e. from /roleRichGroup
+          "pref.customer-user-premium",     // from client role customer-admin-composite-role - realm role for test-app
+          "pref.realm-composite-role",      // from parent group of /roleRichGroup/level2group, i.e. from /roleRichGroup
+          "pref.sample-realm-role"          // from realm role realm-composite-role
+        );
+        assertRoles(testAppMappings,
+          "ta.customer-user",                  // from direct assignment to /roleRichGroup/level2group
+          "ta.customer-admin-composite-role",  // from direct assignment to /roleRichGroup/level2group
+          "ta.customer-admin",                 // from client role customer-admin-composite-role - client role for test-app
+          "ta.sample-client-role"              // from realm role realm-composite-role - client role for test-app
+        );
+    }
+
+    @Test
+    public void testUserGroupRoleToAttributeMappersNotScopedOtherApp() throws Exception {
+        String clientId = "test-app-authz";
+        ProtocolMapperRepresentation realmMapper = ProtocolMapperUtil.createUserRealmRoleMappingMapper("pref.", "Realm roles mapper", "roles-custom.realm", true, true);
+        ProtocolMapperRepresentation clientMapper = ProtocolMapperUtil.createUserClientRoleMappingMapper(clientId, null, "Client roles mapper", "roles-custom." + clientId, true, true);
+
+        ProtocolMappersResource protocolMappers = ApiUtil.findClientResourceByClientId(adminClient.realm("test"), clientId).getProtocolMappers();
+        protocolMappers.createMapper(Arrays.asList(realmMapper, clientMapper));
+
+        // Login user
+        ClientManager.realm(adminClient.realm("test")).clientId(clientId).directAccessGrant(true);
+        oauth.clientId(clientId);
+        OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "rich.roles@redhat.com", "password");
+        IDToken idToken = oauth.verifyIDToken(response.getIdToken());
+
+        // Verify attribute is filled
+        Map<String, Object> roleMappings = (Map<String, Object>)idToken.getOtherClaims().get("roles-custom");
+        Assert.assertThat(roleMappings.keySet(), containsInAnyOrder("realm", clientId));
+        String realmRoleMappings = (String) roleMappings.get("realm");
+        String testAppAuthzMappings = (String) roleMappings.get(clientId);
+        assertRoles(realmRoleMappings,
+          "pref.admin",                     // from direct assignment to /roleRichGroup/level2group
+          "pref.user",                      // from parent group of /roleRichGroup/level2group, i.e. from /roleRichGroup
+          "pref.customer-user-premium",     // from client role customer-admin-composite-role - realm role for test-app
+          "pref.realm-composite-role",      // from parent group of /roleRichGroup/level2group, i.e. from /roleRichGroup
+          "pref.sample-realm-role"          // from realm role realm-composite-role
+        );
+        assertRoles(testAppAuthzMappings);  // There is no client role defined for test-app-authz
+    }
+
+    @Test
+    public void testUserGroupRoleToAttributeMappersScoped() throws Exception {
+        String clientId = "test-app-scope";
+        ProtocolMapperRepresentation realmMapper = ProtocolMapperUtil.createUserRealmRoleMappingMapper("pref.", "Realm roles mapper", "roles-custom.realm", true, true);
+        ProtocolMapperRepresentation clientMapper = ProtocolMapperUtil.createUserClientRoleMappingMapper(clientId, null, "Client roles mapper", "roles-custom.test-app-scope", true, true);
+
+        ProtocolMappersResource protocolMappers = ApiUtil.findClientResourceByClientId(adminClient.realm("test"), clientId).getProtocolMappers();
+        protocolMappers.createMapper(Arrays.asList(realmMapper, clientMapper));
+
+        // Login user
+        ClientManager.realm(adminClient.realm("test")).clientId(clientId).directAccessGrant(true);
+        oauth.clientId(clientId);
+        OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("password", "rich.roles@redhat.com", "password");
+        IDToken idToken = oauth.verifyIDToken(response.getIdToken());
+
+        // Verify attribute is filled
+        Map<String, Object> roleMappings = (Map<String, Object>)idToken.getOtherClaims().get("roles-custom");
+        Assert.assertThat(roleMappings.keySet(), containsInAnyOrder("realm", clientId));
+        String realmRoleMappings = (String) roleMappings.get("realm");
+        String testAppScopeMappings = (String) roleMappings.get(clientId);
+        assertRoles(realmRoleMappings,
+          "pref.admin",                     // from direct assignment to /roleRichGroup/level2group
+          "pref.user"                       // from parent group of /roleRichGroup/level2group, i.e. from /roleRichGroup
+        );
+        assertRoles(testAppScopeMappings,
+          "test-app-allowed-by-scope"       // from direct assignment to roleRichUser, present as scope allows it
+        );
+    }
+
+    @Test
+    public void testUserGroupRoleToAttributeMappersScopedClientNotSet() throws Exception {
+        String clientId = "test-app-scope";
+        ProtocolMapperRepresentation realmMapper = ProtocolMapperUtil.createUserRealmRoleMappingMapper("pref.", "Realm roles mapper", "roles-custom.realm", true, true);
+        ProtocolMapperRepresentation clientMapper = ProtocolMapperUtil.createUserClientRoleMappingMapper(null, null, "Client roles mapper", "roles-custom.test-app-scope", true, true);
+
+        ProtocolMappersResource protocolMappers = ApiUtil.findClientResourceByClientId(adminClient.realm("test"), clientId).getProtocolMappers();
+        protocolMappers.createMapper(Arrays.asList(realmMapper, clientMapper));
+
+        // Login user
+        ClientManager.realm(adminClient.realm("test")).clientId(clientId).directAccessGrant(true);
+        oauth.clientId(clientId);
+        OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("password", "rich.roles@redhat.com", "password");
+        IDToken idToken = oauth.verifyIDToken(response.getIdToken());
+
+        // Verify attribute is filled
+        Map<String, Object> roleMappings = (Map<String, Object>)idToken.getOtherClaims().get("roles-custom");
+        Assert.assertThat(roleMappings.keySet(), containsInAnyOrder("realm", clientId));
+        String realmRoleMappings = (String) roleMappings.get("realm");
+        String testAppScopeMappings = (String) roleMappings.get(clientId);
+        assertRoles(realmRoleMappings,
+          "pref.admin",                     // from direct assignment to /roleRichGroup/level2group
+          "pref.user"                       // from parent group of /roleRichGroup/level2group, i.e. from /roleRichGroup
+        );
+        assertRoles(testAppScopeMappings,
+          "test-app-allowed-by-scope",      // from direct assignment to roleRichUser, present as scope allows it
+          "customer-admin-composite-role"   // from direct assignment to /roleRichGroup/level2group, present as scope allows it
+        );
+    }
+
+    private void assertRoles(String actualRoleString, String...expectedRoles) {
+        String[] roles;
+        Assert.assertThat(actualRoleString.matches("^\\[.*\\]$"), is(true));
+        roles = actualRoleString.substring(1, actualRoleString.length() - 1).split(",\\s*");
+
+        if (expectedRoles == null || expectedRoles.length == 0) {
+            Assert.assertThat(roles, arrayContainingInAnyOrder(""));
+        } else {
+            Assert.assertThat(roles, arrayContainingInAnyOrder(expectedRoles));
+        }
     }
 
 
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/clientreg-test/saml-entity-descriptor.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/clientreg-test/saml-entity-descriptor.xml
index 0bfce4b..694bb82 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/clientreg-test/saml-entity-descriptor.xml
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/clientreg-test/saml-entity-descriptor.xml
@@ -90,10 +90,22 @@ x5Ql0ejivIJAYcMGUyA+/YwJg2FGoA==
             isDefault="true"
             index="0"
             Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact"
-            Location="https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp"/>
+            Location="https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp/artifact"/>
         <AssertionConsumerService
             index="1"
             Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
-            Location="https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp"/>
+            Location="https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp/post"/>
+        <AssertionConsumerService
+            index="2"
+            Binding="urn:oasis:names:tc:SAML:2.0:bindings:PAOS"
+            Location="https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp/paos"/>
+        <AssertionConsumerService
+            index="3"
+            Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
+            Location="https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp/soap"/>
+        <AssertionConsumerService
+            index="4"
+            Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+            Location="https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp/redirect"/>
     </SPSSODescriptor>
 </EntityDescriptor>
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/exportimport-test/kc11-exported-realm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/exportimport-test/kc11-exported-realm.json
index d421fe4..9e76d60 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/exportimport-test/kc11-exported-realm.json
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/exportimport-test/kc11-exported-realm.json
@@ -1,286 +1,4 @@
-[ {
-  "id" : "master",
-  "realm" : "master",
-  "notBefore" : 0,
-  "accessTokenLifespan" : 60,
-  "ssoSessionIdleTimeout" : 1800,
-  "ssoSessionMaxLifespan" : 36000,
-  "accessCodeLifespan" : 60,
-  "accessCodeLifespanUserAction" : 300,
-  "enabled" : true,
-  "sslRequired" : "external",
-  "passwordCredentialGrantAllowed" : false,
-  "registrationAllowed" : false,
-  "rememberMe" : false,
-  "verifyEmail" : false,
-  "resetPasswordAllowed" : false,
-  "social" : false,
-  "updateProfileOnInitialSocialLogin" : false,
-  "bruteForceProtected" : false,
-  "maxFailureWaitSeconds" : 900,
-  "minimumQuickLoginWaitSeconds" : 60,
-  "waitIncrementSeconds" : 60,
-  "quickLoginCheckMilliSeconds" : 1000,
-  "maxDeltaTimeSeconds" : 43200,
-  "failureFactor" : 30,
-  "privateKey" : "MIICXQIBAAKBgQC5lddWO92keqWg+QmMUj/jxA2kwH22UZ0iE9454Ail9JnOvwOTXSP8M92JN7D7DSJM/J45E2Kju5RrQ/QM8bBwYPk/vZlQkJcKbnrkQFtUdBrjoaMQlDvoaqIx1u4irSj2phRPR8teT72A867JGnW2clIwScl2dznZs2Br+jCN3QIDAQABAoGBAKdfFMqnyRfKqM+JaewMTaR7rxZTp8yixET0iCnH++S3uXM03+OqT4bnu7dB67IuwS2Pcp7k9cPWq18l9NcrrcPQCS5knpoNzDO2RuLfXDUCGG/N3MMmthRAeILHun8/CBSfBbcdJESn67g4RV5AldWf8dSgwUcwN4RxbnfUdIbdAkEA9ko38bhfszg9VRea/XVNIpUBQZXpsHt951GoL1Sz0u5iUADyDc/lLgV+eNA9mclvBpg+S+2jcAWMY1rN34wU5wJBAMDm78sYQK8ert+bJV8OSl+6Rpu3cLSdBWNnHZWBpDUHO9JlD2GQblDR3MoL+2j0W/F+7MLhT/LZPQkvMCM+KpsCQGoS+RlQcVc9B51Yd1ZmaPxV9J6MtINgDI/OKYOJFZHpPcp7PcUZHvm9QAVEmuNbUEgk1d/Zz6R1n0tDVpvLN00CQQCH0tNq3DPHWkJlXXdN2+EQUDehMuOfuKPvns5c08CMOgCsHs5asviJ3YqplRA7kTsf6m/ItB637rAkRF6PohkbAkBi9CUTSy32o0AKBuhPDVJOgTqfvlNqmraa/0V65IDhactJ3hmgJXpUI7F0u42NU0uXgU5QMFwHet1sSWxnGcaa",
-  "publicKey" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC5lddWO92keqWg+QmMUj/jxA2kwH22UZ0iE9454Ail9JnOvwOTXSP8M92JN7D7DSJM/J45E2Kju5RrQ/QM8bBwYPk/vZlQkJcKbnrkQFtUdBrjoaMQlDvoaqIx1u4irSj2phRPR8teT72A867JGnW2clIwScl2dznZs2Br+jCN3QIDAQAB",
-  "certificate" : "MIIBlTCB/wIGAVPnGdy9MA0GCSqGSIb3DQEBCwUAMBExDzANBgNVBAMTBm1hc3RlcjAeFw0xNjA0MDUxNTQ0MDVaFw0yNjA0MDUxNTQ1NDVaMBExDzANBgNVBAMTBm1hc3RlcjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAuZXXVjvdpHqloPkJjFI/48QNpMB9tlGdIhPeOeAIpfSZzr8Dk10j/DPdiTew+w0iTPyeORNio7uUa0P0DPGwcGD5P72ZUJCXCm565EBbVHQa46GjEJQ76GqiMdbuIq0o9qYUT0fLXk+9gPOuyRp1tnJSMEnJdnc52bNga/owjd0CAwEAATANBgkqhkiG9w0BAQsFAAOBgQBqQFaqBy50CddfEHPhlf5YDUmTwZIoX/rh74vTESl7thzRQpQ6LhKVI3hfBNI91Xcr58J1WEA3Lm93T7yC5/ShsGbDJi8RJTDhQYY6LBhxT2ZSq+RLFaWyloFLa5V7hTY4F73yml4IM5mKLMmvcxr4xIZvPkKsvR0C+y9yb4dEzg==",
-  "codeSecret" : "8669f9cf-6715-48f0-929d-ec5d66a6efcf",
-  "roles" : {
-    "realm" : [ {
-      "id" : "db49ad9b-6784-4eb8-bedd-07ff716098c0",
-      "name" : "admin",
-      "composite" : true,
-      "composites" : {
-        "realm" : [ "create-realm" ],
-        "application" : {
-          "foo11-realm" : [ "view-events", "view-realm", "manage-events", "manage-clients", "manage-realm", "view-clients", "view-users", "manage-applications", "manage-users", "view-applications" ],
-          "master-realm" : [ "view-realm", "manage-applications", "manage-realm", "manage-users", "view-events", "manage-events", "view-applications", "view-users", "view-clients", "manage-clients" ]
-        }
-      }
-    }, {
-      "id" : "f6b11ea0-0287-4631-9ce1-df4c5998f840",
-      "name" : "create-realm",
-      "composite" : false
-    } ],
-    "application" : {
-      "security-admin-console" : [ ],
-      "foo11-realm" : [ {
-        "id" : "90a00c88-2ad5-4b38-81b2-3ba4583c67c9",
-        "name" : "manage-clients",
-        "composite" : false
-      }, {
-        "id" : "d103fd4a-55f2-409f-8357-5f9645463ac3",
-        "name" : "view-events",
-        "composite" : false
-      }, {
-        "id" : "76952522-6671-4abb-90a9-e6256386b8d3",
-        "name" : "manage-realm",
-        "composite" : false
-      }, {
-        "id" : "973ebcfb-37b2-43ce-af5a-acbc48429c86",
-        "name" : "view-clients",
-        "composite" : false
-      }, {
-        "id" : "b32deca4-a345-4fb6-a6ce-f8666e653c16",
-        "name" : "view-users",
-        "composite" : false
-      }, {
-        "id" : "f030bd3b-3ef8-496c-9c75-f370f19f7a56",
-        "name" : "manage-applications",
-        "composite" : false
-      }, {
-        "id" : "b196345c-07ca-4dea-8a35-84f5aa41f177",
-        "name" : "view-realm",
-        "composite" : false
-      }, {
-        "id" : "747c7af4-60a0-4be4-9c7a-33969572f3e1",
-        "name" : "manage-users",
-        "composite" : false
-      }, {
-        "id" : "ff468d9b-4d5a-4a03-9640-24b0a94a238f",
-        "name" : "manage-events",
-        "composite" : false
-      }, {
-        "id" : "61f9766c-44c2-4195-b9b8-c23d63409c16",
-        "name" : "view-applications",
-        "composite" : false
-      } ],
-      "master-realm" : [ {
-        "id" : "21866bbb-60de-4248-879f-ceb11a75f4e6",
-        "name" : "view-applications",
-        "composite" : false
-      }, {
-        "id" : "267071a5-170f-4438-b333-3d00a0ec268f",
-        "name" : "view-realm",
-        "composite" : false
-      }, {
-        "id" : "53a53160-92b3-43a4-9ba1-a0c19eaf1ad9",
-        "name" : "manage-applications",
-        "composite" : false
-      }, {
-        "id" : "2ce8b8ba-5e15-4a04-bedb-96d74784fd54",
-        "name" : "manage-realm",
-        "composite" : false
-      }, {
-        "id" : "d7045c16-29cb-4e88-bd61-7d6fd77e6c7d",
-        "name" : "manage-users",
-        "composite" : false
-      }, {
-        "id" : "6f933ebd-bbf5-4fea-b4e1-ace854667b9b",
-        "name" : "view-events",
-        "composite" : false
-      }, {
-        "id" : "3588ffcb-96cc-4263-8244-1b71d441202a",
-        "name" : "view-users",
-        "composite" : false
-      }, {
-        "id" : "5a4bcd8f-8cc9-4a01-94d1-3b8a86e228af",
-        "name" : "view-clients",
-        "composite" : false
-      }, {
-        "id" : "5c42606c-f3ec-4abd-aad0-9ec98d6fa39f",
-        "name" : "manage-events",
-        "composite" : false
-      }, {
-        "id" : "678d5c25-b5b0-4447-95c1-b3dc14fa0e3f",
-        "name" : "manage-clients",
-        "composite" : false
-      } ],
-      "account" : [ {
-        "id" : "700d3f40-8e11-47d7-b3f1-14d07a7da647",
-        "name" : "manage-account",
-        "composite" : false
-      }, {
-        "id" : "a9d81246-ec6c-4b71-912a-7a1518ec64d5",
-        "name" : "view-profile",
-        "composite" : false
-      } ]
-    }
-  },
-  "requiredCredentials" : [ "password" ],
-  "users" : [ {
-    "id" : "d678f579-29f4-46d5-a124-8bcdbeeeb55d",
-    "username" : "admin",
-    "enabled" : true,
-    "totp" : false,
-    "emailVerified" : false,
-    "credentials" : [ {
-      "type" : "password",
-      "hashedSaltedValue" : "VIw4dTFMrU8aw3xvsI6Kqh2gA5Y0P2TJEyEmgplkColwuXUC2G+RTsahsOgqwG9yIgyrFS9Fe+GlPNUQWxO1Sw==",
-      "salt" : "5IsVTxiv9At7xTHoTN17+g==",
-      "hashIterations" : 1,
-      "temporary" : false
-    } ],
-    "requiredActions" : [ ],
-    "realmRoles" : [ "admin" ],
-    "applicationRoles" : {
-      "account" : [ "manage-account", "view-profile" ]
-    }
-  } ],
-  "scopeMappings" : [ {
-    "client" : "security-admin-console",
-    "roles" : [ "admin" ]
-  } ],
-  "applications" : [ {
-    "id" : "4fe35549-1d84-440e-83c6-48cad624aba4",
-    "name" : "master-realm",
-    "surrogateAuthRequired" : false,
-    "enabled" : true,
-    "secret" : "0da9f8c5-ee7a-4d4b-9c93-944ac72b7ef0",
-    "redirectUris" : [ ],
-    "webOrigins" : [ ],
-    "claims" : {
-      "name" : true,
-      "username" : true,
-      "profile" : true,
-      "picture" : true,
-      "website" : true,
-      "email" : true,
-      "gender" : true,
-      "locale" : true,
-      "address" : true,
-      "phone" : true
-    },
-    "notBefore" : 0,
-    "bearerOnly" : true,
-    "publicClient" : false,
-    "attributes" : { },
-    "fullScopeAllowed" : true,
-    "nodeReRegistrationTimeout" : 0
-  }, {
-    "id" : "5b2a2ae8-f0b9-40cc-a586-4adf46379a49",
-    "name" : "account",
-    "baseUrl" : "/auth/realms/master/account",
-    "surrogateAuthRequired" : false,
-    "enabled" : true,
-    "secret" : "f055644e-e59e-462f-98fd-9a5b7c22e03a",
-    "defaultRoles" : [ "view-profile", "manage-account" ],
-    "redirectUris" : [ "/auth/realms/master/account/*" ],
-    "webOrigins" : [ ],
-    "claims" : {
-      "name" : true,
-      "username" : true,
-      "profile" : true,
-      "picture" : true,
-      "website" : true,
-      "email" : true,
-      "gender" : true,
-      "locale" : true,
-      "address" : true,
-      "phone" : true
-    },
-    "notBefore" : 0,
-    "bearerOnly" : false,
-    "publicClient" : false,
-    "attributes" : { },
-    "fullScopeAllowed" : false,
-    "nodeReRegistrationTimeout" : 0
-  }, {
-    "id" : "22ed594d-8c21-43f0-a080-c8879a411f94",
-    "name" : "security-admin-console",
-    "baseUrl" : "/auth/admin/master/console/index.html",
-    "surrogateAuthRequired" : false,
-    "enabled" : true,
-    "secret" : "bf9b9f8b-0a85-42da-bc14-befab4305298",
-    "redirectUris" : [ "/auth/admin/master/console/*" ],
-    "webOrigins" : [ ],
-    "claims" : {
-      "name" : true,
-      "username" : true,
-      "profile" : true,
-      "picture" : true,
-      "website" : true,
-      "email" : true,
-      "gender" : true,
-      "locale" : true,
-      "address" : true,
-      "phone" : true
-    },
-    "notBefore" : 0,
-    "bearerOnly" : false,
-    "publicClient" : true,
-    "attributes" : { },
-    "fullScopeAllowed" : false,
-    "nodeReRegistrationTimeout" : 0
-  }, {
-    "id" : "c9c3bd5f-b69d-4640-8b27-45d4f3866a36",
-    "name" : "foo11-realm",
-    "surrogateAuthRequired" : false,
-    "enabled" : true,
-    "secret" : "aba746f8-fafd-4d6d-af65-e0bb669b1afc",
-    "redirectUris" : [ ],
-    "webOrigins" : [ ],
-    "claims" : {
-      "name" : true,
-      "username" : true,
-      "profile" : true,
-      "picture" : true,
-      "website" : true,
-      "email" : true,
-      "gender" : true,
-      "locale" : true,
-      "address" : true,
-      "phone" : true
-    },
-    "notBefore" : 0,
-    "bearerOnly" : true,
-    "publicClient" : false,
-    "attributes" : { },
-    "fullScopeAllowed" : true,
-    "nodeReRegistrationTimeout" : 0
-  } ],
-  "oauthClients" : [ ],
-  "browserSecurityHeaders" : {
-    "xFrameOptions" : "SAMEORIGIN",
-    "contentSecurityPolicy" : "frame-src 'self'"
-  },
-  "socialProviders" : { },
-  "smtpServer" : { },
-  "eventsEnabled" : false,
-  "eventsListeners" : [ ]
-}, {
+{
   "id" : "14e6923c-f5fb-44aa-8982-35d4976c56c5",
   "realm" : "foo11",
   "notBefore" : 0,
@@ -491,4 +209,4 @@
   "smtpServer" : { },
   "eventsEnabled" : false,
   "eventsListeners" : [ ]
-} ]
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json
index d8c4dc1..deb5b64 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json
@@ -119,7 +119,10 @@
         "default": {
             "clustered": "${keycloak.connectionsInfinispan.clustered:false}",
             "async": "${keycloak.connectionsInfinispan.async:true}",
-            "sessionsOwners": "${keycloak.connectionsInfinispan.sessionsOwners:2}"
+            "sessionsOwners": "${keycloak.connectionsInfinispan.sessionsOwners:2}",
+            "remoteStoreEnabled": "${keycloak.connectionsInfinispan.remoteStoreEnabled:false}",
+            "remoteStoreHost": "${keycloak.connectionsInfinispan.remoteStoreHost:localhost}",
+            "remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort:11222}"
         }
     },
 
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json
index c0b2b6c..b0e8767 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json
@@ -85,6 +85,21 @@
       "groups": [
         "/topGroup/level2group"
       ]
+    },
+    {
+      "username" : "roleRichUser",
+      "enabled": true,
+      "email" : "rich.roles@redhat.com",
+      "credentials" : [
+        { "type" : "password",
+          "value" : "password" }
+      ],
+      "groups": [
+        "/roleRichGroup/level2group"
+      ],
+      "clientRoles": {
+        "test-app-scope": [ "test-app-allowed-by-scope", "test-app-disallowed-by-scope" ]
+      }
     }
   ],
   "scopeMappings": [
@@ -95,6 +110,10 @@
     {
       "client": "test-app",
       "roles": ["user"]
+    },
+    {
+      "client": "test-app-scope",
+      "roles": ["user", "admin"]
     }
   ],
   "clients": [
@@ -109,6 +128,16 @@
       "secret": "password"
     },
     {
+      "clientId" : "test-app-scope",
+      "enabled": true,
+
+      "redirectUris": [
+        "http://localhost:8180/auth/realms/master/app/*"
+      ],
+      "secret": "password",
+      "fullScopeAllowed": "false"
+    },
+    {
       "clientId" : "third-party",
       "enabled": true,
       "consentRequired": true,
@@ -290,6 +319,22 @@
       {
         "name": "customer-user-premium",
         "description": "Have User Premium privileges"
+      },
+      {
+        "name": "sample-realm-role",
+        "description": "Sample realm role"
+      },
+      {
+        "name": "realm-composite-role",
+        "description": "Realm composite role containing client role",
+        "composite" : true,
+        "composites" : {
+          "realm" : [ "sample-realm-role" ],
+          "client" : {
+            "test-app" : [ "sample-client-role" ],
+            "account" : [ "view-profile" ]
+          }
+        }
       }
     ],
     "client" : {
@@ -301,6 +346,31 @@
         {
           "name": "customer-admin",
           "description": "Have Customer Admin privileges"
+        },
+        {
+          "name": "sample-client-role",
+          "description": "Sample client role"
+        },
+        {
+          "name": "customer-admin-composite-role",
+          "description": "Have Customer Admin privileges via composite role",
+          "composite" : true,
+          "composites" : {
+            "realm" : [ "customer-user-premium" ],
+            "client" : {
+              "test-app" : [ "customer-admin" ]
+            }
+          }
+        }
+      ],
+      "test-app-scope" : [
+        {
+          "name": "test-app-allowed-by-scope",
+          "description": "Role allowed by scope in test-app-scope"
+        },
+        {
+          "name": "test-app-disallowed-by-scope",
+          "description": "Role disallowed by scope in test-app-scope"
         }
       ]
     }
@@ -328,6 +398,31 @@
           }
         }
       ]
+    },
+    {
+      "name": "roleRichGroup",
+      "attributes": {
+        "topAttribute": ["true"]
+
+      },
+      "realmRoles": ["user", "realm-composite-role"],
+      "clientRoles": {
+        "account": ["manage-account"]
+      },
+
+      "subGroups": [
+        {
+          "name": "level2group",
+          "realmRoles": ["admin"],
+          "clientRoles": {
+            "test-app": ["customer-user", "customer-admin-composite-role"]
+          },
+          "attributes": {
+            "level2Attribute": ["true"]
+
+          }
+        }
+      ]
     }
   ],
 
@@ -337,6 +432,16 @@
       {
         "client": "third-party",
         "roles": ["customer-user"]
+      },
+      {
+        "client": "test-app-scope",
+        "roles": ["customer-admin-composite-role"]
+      }
+    ],
+    "test-app-scope": [
+      {
+        "client": "test-app-scope",
+        "roles": ["test-app-allowed-by-scope"]
       }
     ]
   },
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/example/authorization/EAP6DefaultAuthzConfigAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/example/authorization/EAP6DefaultAuthzConfigAdapterTest.java
new file mode 100644
index 0000000..d7fe93a
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/example/authorization/EAP6DefaultAuthzConfigAdapterTest.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.adapter.example.authorization;
+
+import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
+
+/**
+ *
+ * @author tkyjovsk
+ */
+@AppServerContainer("app-server-eap6")
+public class EAP6DefaultAuthzConfigAdapterTest extends AbstractDefaultAuthzConfigAdapterTest {
+
+}
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/example/authorization/EAP6PhotozExampleAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/example/authorization/EAP6PhotozExampleAdapterTest.java
new file mode 100644
index 0000000..7319dce
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/example/authorization/EAP6PhotozExampleAdapterTest.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.adapter.example.authorization;
+
+import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
+
+/**
+ *
+ * @author tkyjovsk
+ */
+@AppServerContainer("app-server-eap6")
+//@AdapterLibsLocationProperty("adapter.libs.wildfly")
+public class EAP6PhotozExampleAdapterTest extends AbstractPhotozExampleAdapterTest {
+
+}
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/example/authorization/EAP6ServletAuthzAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/example/authorization/EAP6ServletAuthzAdapterTest.java
new file mode 100644
index 0000000..5833b29
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/example/authorization/EAP6ServletAuthzAdapterTest.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.adapter.example.authorization;
+
+import org.jboss.arquillian.container.test.api.RunAsClient;
+import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
+
+/**
+ *
+ * @author tkyjovsk
+ */
+@RunAsClient
+@AppServerContainer("app-server-eap6")
+public class EAP6ServletAuthzAdapterTest extends AbstractServletAuthzAdapterTest {
+
+}
diff --git a/testsuite/integration-arquillian/tests/other/clean-start/pom.xml b/testsuite/integration-arquillian/tests/other/clean-start/pom.xml
index 65e00bd..06f6ffe 100644
--- a/testsuite/integration-arquillian/tests/other/clean-start/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/clean-start/pom.xml
@@ -23,7 +23,7 @@
     <parent>
         <groupId>org.keycloak.testsuite</groupId>
         <artifactId>integration-arquillian-tests-other</artifactId>
-        <version>2.1.0-SNAPSHOT</version>
+        <version>2.4.0.CR1-SNAPSHOT</version>
     </parent>
     
     <artifactId>integration-arquillian-tests-smoke-clean-start</artifactId>
diff --git a/testsuite/integration-arquillian/tests/other/pom.xml b/testsuite/integration-arquillian/tests/other/pom.xml
index a36c641..ed5e119 100644
--- a/testsuite/integration-arquillian/tests/other/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/pom.xml
@@ -119,6 +119,12 @@
             </dependencies>
         </profile>
         <profile>
+            <id>clean-start</id>
+            <modules>
+                <module>clean-start</module>
+            </modules>
+        </profile>
+        <profile>
             <id>console-ui-tests</id>
             <modules>
                 <module>console</module>
diff --git a/testsuite/integration-arquillian/tests/other/sssd/src/test/java/org/keycloak/testsuite/sssd/SSSDTest.java b/testsuite/integration-arquillian/tests/other/sssd/src/test/java/org/keycloak/testsuite/sssd/SSSDTest.java
index b11d5e4..68488cc 100644
--- a/testsuite/integration-arquillian/tests/other/sssd/src/test/java/org/keycloak/testsuite/sssd/SSSDTest.java
+++ b/testsuite/integration-arquillian/tests/other/sssd/src/test/java/org/keycloak/testsuite/sssd/SSSDTest.java
@@ -2,6 +2,7 @@ package org.keycloak.testsuite.sssd;
 
 import org.jboss.arquillian.graphene.page.Page;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.keycloak.representations.idm.GroupRepresentation;
@@ -72,6 +73,7 @@ public class SSSDTest extends AbstractKeycloakTest {
         adminClient.realm(REALM_NAME).userFederation().create(userFederation);
     }
 
+    @Ignore
     @Test
     public void testProviderFactories() {
         List<UserFederationProviderFactoryRepresentation> providerFactories = adminClient.realm(REALM_NAME).userFederation().getProviderFactories();
diff --git a/testsuite/integration-arquillian/tests/pom.xml b/testsuite/integration-arquillian/tests/pom.xml
index 4f4f42d..f6838ab 100755
--- a/testsuite/integration-arquillian/tests/pom.xml
+++ b/testsuite/integration-arquillian/tests/pom.xml
@@ -182,6 +182,8 @@
                             <firefox_binary>${firefox_binary}</firefox_binary>
 
                             <project.version>${project.version}</project.version>
+                            <migration.project.version>${migration.project.version}</migration.project.version>
+                            <migration.product.version>${migration.product.version}</migration.product.version>
                         </systemPropertyVariables>
                         <properties>
                             <property>
diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_lt.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_lt.properties
index c40c438..d4ffc54 100644
--- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_lt.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_lt.properties
@@ -120,7 +120,7 @@ realm-tab-email=El. pa\u0161tas
 realm-tab-themes=Temos
 realm-tab-cache=Pod\u0117lis
 realm-tab-tokens=Raktai
-realm-tab-client-initial-access=Pradiniai prieigos raktai
+realm-tab-client-registration=Klient\u0173 registracija
 realm-tab-security-defenses=Saugos priemon\u0117s
 realm-tab-general=Bendra informacija
 add-realm=Prid\u0117ti srit\u012F
@@ -207,6 +207,8 @@ include-authnstatement=\u012Etraukti AuthnStatement
 include-authnstatement.tooltip=Ar prisijungimo b\u016Bdas ir laikas \u0161ur\u0117t\u0173 b\u016Bti \u012Ftraukiami \u012F prisijungimo operacijos atsakym\u0105?
 sign-documents=Pasira\u0161yti dokumentus
 sign-documents.tooltip=Ar SAML dokumentai turi b\u016Bt\u012F pasira\u0161omi \u0161ios srities?
+sign-documents-redirect-enable-key-info-ext=Optimizuoti REDIRECT pasira\u0161ymo rakto paie\u0161k\u0105
+sign-documents-redirect-enable-key-info-ext.tooltip=Ar privalo b\u016Bti itrauktas pasira\u0161ymo rakto ID \u012F SAML protokolo \u017Einut\u0117s <Extensions> element\u0105 kuomet pasira\u0161omi Keycloak REDIRECT SP s\u0105sajos dokumentai? Tokiu b\u016Bdu tikrinan\u010Dioji pus\u0117 optimizuoja tikrinimo proce\u0105 naudodama tik vien\u0105 rakt\u0105 vietoj to, kad bandyt\u0173 vis\u0173 rakt\u0173 kombinacijas.
 sign-assertions=Pasira\u0161yti sprendinius
 sign-assertions.tooltip=Ar SAML sprendiniai SAML dokumentuose turi b\u016Bti pasira\u0161omi? \u0160is nustatymas neb\u016Btinas, kuomet naudojamas viso dokumento pasira\u0161ymas.
 signature-algorithm=Para\u0161o algoritmas
@@ -432,6 +434,10 @@ store-tokens=Saugoti raktus
 identity-provider.store-tokens.tooltip=Jei \u012Fgalinta, tuomet po naudotoj\u0173 prisijungimo, prieigos raktai bus i\u0161saugoti.
 stored-tokens-readable=Saugoti raktus skaitomame formate
 identity-provider.stored-tokens-readable.tooltip=Jei \u012Fgalinta, tuomet naudotojai gali per\u017Ei\u016Br\u0117ti i\u0161saugotus prieigos raktus. \u012Egalinama broker.read-token rol\u0117.
+disableUserInfo=U\u017Edrausti naudotojo informacijos prieig\u0105
+identity-provider.disableUserInfo.tooltip=Ar u\u017Edrausti prieig\u0105 prie papildomos naudotojo profilio informacijos per User Info paslaug\u0105? Numatyta reik\u0161m\u0117 - naudoti \u0161i\u0105 OIDC paslaug\u0105.
+userIp=Naudoti userIp parametr\u0105
+identity-provider.google-userIp.tooltip=Ar kvie\u010Diant Google naudotojo informacijos paslaug\u0105 naudoti 'userIp' u\u017Eklausos parametr\u0105?  Nusta\u010Dius bus naudojamas naudotojo IP adresas.  Nustatymas naudingas tuo atveju, jei Google ribot\u0173 u\u017Eklaus\u0173 kiek\u0161 i\u0161 vieno IP adreso.
 update-profile-on-first-login=Profilio duomen\u0173 atnaujinimas pirmojo prisijungimo metu
 on=On
 off=Off
@@ -500,8 +506,8 @@ force-authentication=Priverstin\u0117 autentifikacija
 identity-provider.force-authentication.tooltip=Jei \u012Fgalinta, tuomet tapatyb\u0117s teik\u0117jas privalo autentifikuoti naudotoj\u0105 i\u0161 naujo nepasitikint ankstesniu prisijungimu.
 validate-signature=Para\u0161o tikrinimas
 saml.validate-signature.tooltip=\u012Ejungti/i\u0161jungti SAML atsakym\u0173 para\u0161o tikrinim\u0105.
-validating-x509-certificate=X509 sertifikatas tikrinimui
-validating-x509-certificate.tooltip=PEM formato sertifikatas, kuris turi b\u016Bti naudojamas para\u0161\u0173 tikrinimui.
+validating-x509-certificate=X509 sertifikatai tikrinimui
+validating-x509-certificate.tooltip=PEM formato sertifikatai, kurie turi b\u016Bti naudojami para\u0161\u0173 tikrinimui. Reik\u0161m\u0117s skiriamos kableliais (,).
 saml.import-from-url.tooltip=Importuoti metaduomenis i\u0161 nutolusio IDP SAML subjekto apra\u0161o.
 social.client-id.tooltip=Kliento identifikatorius u\u017Eregistruotas tapatyb\u0117s teik\u0117jo sistemoje.
 social.client-secret.tooltip=Kliento saugos kodas u\u017Eregistruotas tapatyb\u0117s teik\u0117jo sistemoje.
@@ -532,6 +538,7 @@ remainingCount=Lik\u0119s kiekis
 created=Sukurta
 back=Atgal
 initial-access-tokens=Pradiniai prieigos raktai
+initial-access-tokens.tooltip=Pradiniai prieigos raktai naudojami klient\u0173 registracijoms dinaminiu b\u016Bdu. U\u017Eklausos su \u0161iais raktais gali b\u016Bti siun\u010Diami i\u0161 bet kurio serverio.
 add-initial-access-tokens=Prid\u0117ti pradin\u012F prieigos rakt\u0105
 initial-access-token=Pradinis prieigos raktas
 initial-access.copyPaste.tooltip=Nukopijuokite ir \u012Fklijuokite prieigos rakt\u0105 prie\u0161 i\u0161eidami i\u0161 \u0161io puslapio. V\u0117liau negal\u0117site kopijuoti \u0161i\u0173 prieigos rakt\u0173.
@@ -540,15 +547,29 @@ initial-access-token.confirm.title=Kopijuoti pradinius prieigos raktus
 initial-access-token.confirm.text=Pra\u0161ome \u012Fsitikinti, kad nusikopijavote pradinius prieigos raktus nes v\u0117liau prie rakt\u0173 nebegal\u0117site prieiti
 no-initial-access-available=N\u0117ra galim\u0173 pradini\u0173 prieigos rak\u0161\u0173
 
-trusted-hosts-legend=Patikimi kliento registracijos serveriai
-trusted-hosts-legend.tooltip=Serveri\u0173 vardai, kuriais pasitikima kliento registracijos metu. Klient\u0173 registravimo u\u017Eklausos i\u0161 \u0161i\u0173 serveri\u0173 gali b\u016Bti siun\u010Diamos be pradini\u0173 prieigos rakt\u0173. Klient\u0173 registracijos skai\u010Dius ribojamas pagal nurodyt\u0105 kiekvieno serverio limit\u0105.
-no-client-trusted-hosts-available=N\u0117ra galim\u0173 patikim\u0173 serveri\u0173
-add-client-reg-trusted-host=Prid\u0117ti patikim\u0105 server\u012F
-hostname=Serverio vardas
-client-reg-hostname.tooltip=Pilnas serverio vardas arba IP adresas. Klient\u0173 registracijomis su \u0161iuo serverio vardu arba IP adresu bus pasitikima ir leid\u017Eiama nauj\u0173 klient\u0173 registracija.
-client-reg-count.tooltip=Limitas, kiek registravimo u\u017Eklaus\u0173 galima atsi\u0173sti i\u0161 kiekvieno serverio. Limitas bus atkurtas tik po atk\u016Brimo.
-client-reg-remainingCount.tooltip=I\u0161 \u0161io serverio lik\u0119s galim\u0173 registracijos u\u017Eklaus\u0173 skai\u010Dius. Limitas bus atkurtas tik po atk\u016Brimo.
-reset-remaining-count=Atk\u016Brimo limit\u0105
+client-reg-policies=Klient\u0173 registravimo taisykl\u0117s
+client-reg-policy.name.tooltip=Taisykl\u0117s rodomas pavadinimas
+anonymous-policies=Anonimin\u0117s prieigos taisykl\u0117s
+anonymous-policies.tooltip=\u0160ios taisykl\u0117s naudojamos tuomet, kai klient\u0173 registravimo paslauga i\u0161kvie\u010Diama neautentifikuota u\u017Eklausa. T.y. u\u017Eklausa neturi nei pradini\u0173 prieigos rakt\u0173 (Initial Access Token) nei prieigos rakt\u0173 (Bearer Token).
+auth-policies=Autentifikuotos prieigos taisykl\u0117s
+auth-policies.tooltip=\u0160ios taisykl\u0117s naudojamos tuomet, kai klient\u0173 registravimo paslauga i\u0161kvie\u010Diama autentifikuota u\u017Eklausa. T.y. u\u017Eklausa turi pradini\u0173 prieigos rakt\u0173 (Initial Access Token) arba prieigos rakt\u0173 (Bearer Token).
+policy-name=Taisykl\u0117s pavadinimas
+no-client-reg-policies-configured=N\u0117ra klient\u0173 registravimo taisykli\u0173
+trusted-hosts.label=Patikimi serveriai
+trusted-hosts.tooltip=Serveri\u0173 s\u0105ra\u0161as, kuriems suteikiama teis\u0117 kviesti klient\u0173 registravimo paslaug\u0105 (Client Registration Service) ir/arba naudototi \u0161ias reik\u0161mes klient\u0173 URI parametre (Client URI). Galima naudoti serveri\u0173 vardus arba IP adresus. Jei kaip pirmas simbolis naudojamas i\u0161ple\u010Diantis simbolis (pvz '*.example.com') tuomet visas domenas 'example.com' bus patikimas.
+host-sending-registration-request-must-match.label=Klient\u0173 registracijos paslaugos naudotojo serverio vardas turi sutapti 
+host-sending-registration-request-must-match.tooltip=Jei \u0161galinta, tuomet visos klient\u0173 registravimo u\u017Eklausos leid\u017Eiamos tik tuo atveju, jei jos buvo i\u0161si\u0173stos i\u0161 to pa\u010Dio patikimo serverio ar domeno.
+client-uris-must-match.label=Klient\u0173 URI turi sutapti
+client-uris-must-match.tooltip=Jei \u012Fgalinta, tuomet visos klient\u0173 nuorodos (nukreipimo nuorodos ir kitos) leid\u017Eiamos tik tuo atveju, jei jos sutampa su patikimu serverio vardu arba domenu.
+allowed-protocol-mappers.label=Leid\u017Eiami protokolo atitikmen\u0173 parink\u0117jai
+allowed-protocol-mappers.tooltip=Nurodykite visus leid\u017Eiamus protokolo atitikmen\u0173 parink\u0117jus. Jei bandoma registruoti klient\u0105, kuris turi protokolo atitikmen\u0173 parink\u0117j\u0105 ne\u0161traukt\u0105 \u0161 leid\u017Eiam\u0173 s\u0105ra\u0161\u0105, tuomet visa registracijos u\u017Eklausa bus atmesta.
+consent-required-for-all-mappers.label=Privalomas vis\u0173 atitikmen\u0173 parink\u0117j\u0173 pritarimas
+consent-required-for-all-mappers.label=Consent Required For Mappers
+consent-required-for-all-mappers.tooltip=Jei \u012Fgalinta, tuomet visi naujai u\u017Eregistruotiems protokolo parink\u0117jams automati\u0161kai \u012Fgalinama consentRequired opcija. Tai rei\u0161kia, kad naudotojas privalo pateikti patvirtinim\u0105. PASTABA: Patvirtinimo ekranas rodomas tik tiems klientams, kuriems \u012Fjungtas consentRequired nustatymas. Da\u017Eniausiai geriausia nustatyti \u0161i\u0105 nuostat\u0105 kartu su consent-required taisykle.
+allowed-client-templates.label=Leid\u017Eiami klient\u0173 \u0161ablonai
+allowed-client-templates.tooltip=Leid\u017Eiam\u0173 kliento \u0161ablon\u0173 s\u0105ra\u0161as, kuriuos galima naudoti naujai registruojamiems klientams. Bandant registruoti klient\u0105 naudojant kliento \u0161ablon\u0105, kurio n\u0117ra s\u0105ra\u0161e bus atmestas. Pradin\u0117 reik\u0161m\u0117 - tu\u0161\u010Dias s\u0105ra\u0161as, t.y. neleid\u017Eiamas nei vienas kliento \u0161ablonas. 
+max-clients.label=Mksimalus srities klient\u0173 skai\u010Dius
+max-clients.tooltip=Nauj\u0173 klient\u0173 registracija draud\u017Eiama, jei u\u017Eregistruot\u0173 klient\u0173 skai\u010Dius yra toks pats arba didesnis nei nustatytas limitas.
 
 client-templates=Klient\u0173 \u0161ablonai
 client-templates.tooltip=Klient\u0173 \u0161ablonai leid\u017Eia nurodyti bendr\u0105 vis\u0173 klient\u0173 konfig\u016Bracij\u0105
@@ -880,6 +901,8 @@ spi=SPI
 granted-roles=Suteiktos rol\u0117s
 granted-protocol-mappers=Suteiktos protokolo atitikmen\u0173 s\u0105sajos
 additional-grants=Papildomai suteikta
+consent-created-date=Sukurta
+consent-last-updated-date=Pask. kart\u0105 atnaujinta
 revoke=At\u0161aukti
 new-password=Naujas slapta\u017Eodis
 password-confirmation=Pakartotas slapta\u017Eodis
@@ -1144,3 +1167,52 @@ authz-evaluation-policies.tooltip=Informacija apie vertinime dalyvavusias taisyk
 authz-evaluation-authorization-data=Atsakymas
 authz-evaluation-authorization-data.tooltip=Autorizavimo u\u017Eklausos apdorojimo rezultatas su autorizacijos duomenimis. Rezultatas parodo k\u0105 Keycloak gr\u0105\u017Eina klientui pra\u0161an\u010Diam leidimo. Per\u017Ei\u016Br\u0117kite 'authorization' teigin\u012F su leidimais, kurie buvo suteikti \u0161iai autorizacijos u\u017Eklausai.
 authz-show-authorization-data=Rodyti autorizacijos duomenis
+
+kid=KID
+keys=Raktai
+all=Visi
+status=B\u016Bsena
+keystore=Rakt\u0173 saugykla
+keystores=Rakt\u0173 saugyklos
+add-keystore=Prid\u0117ti rakt\u0173 saugykl\u0105
+add-keystore.placeholder=Prid\u0117ti rakt\u0173 saugykl\u0105...
+view=\u017Di\u016Br\u0117ti
+active=Aktyvus
+
+Sunday=Sekmadienis
+Monday=Pirmadienis
+Tuesday=Antradienis
+Wednesday=Tre\u010Diadienis
+Thursday=Ketvirtadienis
+Friday=Penktadienis
+Saturday=\u0160e\u0161tadienis
+
+user-storage-cache-policy=Pod\u0117lio nustatymai
+userStorage.cachePolicy=Pod\u0117lio taisykl\u0117s
+userStorage.cachePolicy.option.DEFAULT=DEFAULT
+userStorage.cachePolicy.option.EVICT_WEEKLY=EVICT_WEEKLY
+userStorage.cachePolicy.option.EVICT_DAILY=EVICT_DAILY
+userStorage.cachePolicy.option.MAX_LIFESPAN=MAX_LIFESPAN
+userStorage.cachePolicy.option.NO_CACHE=NO_CACHE
+userStorage.cachePolicy.tooltip=Saugyklos teik\u0117jo pod\u0117lio nustatymai.  'DEFAULT' naudojami numatytieji globalaus naudotojo pod\u0117lio nustatymai.  'EVICT_DAILY' naudotoj\u0173 pod\u0117lis i\u0161valomas kiekvien\u0105 dien\u0105 numatytuoju laiku.  'EVICT_WEEKLY' naudotoj\u0173 pod\u0117lis i\u0161valomas kart\u0105 \u012F savait\u0119 numatyt\u0105 dien\u0105.  'MAX-LIFESPAN' maksimalus pod\u0117lio \u012Fra\u0161o galiojimo laikas milisekund\u0117mis.
+userStorage.cachePolicy.evictionDay=I\u0161valymo diena
+userStorage.cachePolicy.evictionDay.tooltip=Savait\u0117s diena, kuomet pod\u0117lio \u012Fra\u0161ai taps nebeaktual\u016Bs
+userStorage.cachePolicy.evictionHour=I\u0161valymo valanda
+userStorage.cachePolicy.evictionHour.tooltip=Valanda, kuomet pod\u0117lio \u012Fra\u0161ai taps nebeaktual\u016Bs.
+userStorage.cachePolicy.evictionMinute=I\u0161valymo minut\u0117
+userStorage.cachePolicy.evictionMinute.tooltip=Minut\u0117, kuomet pod\u0117lio \u012Fra\u0161ai taps nebeaktual\u016Bs.
+userStorage.cachePolicy.maxLifespan=Maksimalus galiojimo laikas
+userStorage.cachePolicy.maxLifespan.tooltip=Maksimalus galiojimo laikas milisekund\u0117mis po kurio pod\u0117lio \u012Fra\u0161ai taps nebeaktual\u016Bs.
+user-origin-link=Saugojimo kilm\u0117
+
+disable=I\u0161jungti
+disableable-credential-types=I\u0161jungiami tipai
+credentials.disableable.tooltip=Galim\u0173 i\u0161jungti prisijungimo duomen\u0173 tip\u0173 s\u0105ra\u0161as
+disable-credential-types=I\u0161jungti prisijungimo duomen\u0173 tipus
+credentials.disable.tooltip=Paspauskite mygtuk\u0105 nor\u0117dami i\u0161jungti pa\u017Eym\u0117tus prisijungimo duomen\u0173 tipus
+credential-types=Prisijungimo duomen\u0173 tipai
+manage-user-password=Tvarkyti slapta\u017Eod\u017Eius
+disable-credentials=I\u0161jungti prisijungimo duomenis
+credential-reset-actions=Prisijungimo duomen\u0173 atk\u016Brimas
+ldap-mappers=LDAP atitikmen\u0173 parink\u0117jai
+create-ldap-mapper=Sukurti LDAP atitikmen\u0173 parink\u0117j\u0105
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
index 624e9a5..564313c 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
@@ -1352,7 +1352,11 @@ module.controller('ClientScopeMappingCtrl', function($scope, $http, realm, clien
 
     }
 
-
+    $scope.hideRoleSelector = function() {
+       return ($scope.client.useTemplateScope && $scope.template && template.fullScopeAllowed)
+               || (!$scope.template && $scope.client.fullScopeAllowed);
+    }
+    
     $scope.changeFlag = function() {
         Client.update({
             realm : realm.realm,
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
index c3b7e1f..36b432f 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
@@ -337,7 +337,7 @@ module.controller('UserTabCtrl', function($scope, $location, Dialog, Notificatio
 module.controller('UserDetailCtrl', function($scope, realm, user, BruteForceUser, User,
                                              Components,
                                              UserFederationInstances, UserImpersonation, RequiredActions,
-                                             $location, Dialog, Notifications) {
+                                             $location, $http, Dialog, Notifications) {
     $scope.realm = realm;
     $scope.create = !user.id;
     $scope.editUsername = $scope.create || $scope.realm.editUsernameAllowed;
@@ -362,7 +362,16 @@ module.controller('UserDetailCtrl', function($scope, realm, user, BruteForceUser
             });
         };
         if(user.federationLink) {
-            console.log("federationLink is not null");
+            console.log("federationLink is not null. It is " + user.federationLink);
+
+            // TODO: This is temporary and should be removed once we remove userFederation SPI. It can be replaced with Components.get below
+            var fedUrl = authUrl + '/admin/realms/' + realm.realm + '/user-federation/instances-with-fallback/' + user.federationLink;
+            $http.get(fedUrl).success(function(data, status, headers, config) {
+                $scope.federationLinkName = data.federationLinkName;
+                $scope.federationLink = data.federationLink;
+            });
+
+            /*
             if (user.federationLink.startsWith('f:')) {
                  Components.get({realm: realm.realm, componentId: user.federationLink}, function (link) {
                     $scope.federationLinkName = link.name;
@@ -373,7 +382,7 @@ module.controller('UserDetailCtrl', function($scope, realm, user, BruteForceUser
                     $scope.federationLinkName = link.displayName;
                     $scope.federationLink = "#/realms/" + realm.realm + "/user-federation/providers/" + link.providerName + "/" + link.id;
                 });
-            }
+            }*/
 
         } else {
             console.log("federationLink is null");
@@ -627,14 +636,21 @@ module.controller('UserFederationCtrl', function($scope, $location, $route, real
     for (var i = 0; i < $scope.providers.length; i++) {
         $scope.providers[i].isUserFederationProvider = false;
     }
-    /*
+
     UserFederationProviders.query({realm: realm.realm}, function(data) {
         for (var i = 0; i < data.length; i++) {
             data[i].isUserFederationProvider = true;
+
+            var existingProvider = $scope.providers.find(function(provider){ return provider.id == data[i].id });
+            if (existingProvider) {
+                angular.copy(data[i], existingProvider);
+                continue;
+            }
+
             $scope.providers.push(data[i]);
         }
     });
-    */
+
 
     $scope.addProvider = function(provider) {
         console.log('Add provider: ' + provider.id);
@@ -1712,6 +1728,7 @@ module.controller('LDAPUserStorageCtrl', function($scope, $location, Notificatio
         }
 
         $scope.changed = false;
+        $scope.lastVendor = instance.config['vendor'][0];
     }
 
     initUserStorageSettings();
@@ -1724,7 +1741,7 @@ module.controller('LDAPUserStorageCtrl', function($scope, $location, Notificatio
         }
 
         if (!angular.equals($scope.instance.config['vendor'][0], $scope.lastVendor)) {
-            console.log("LDAP vendor changed");
+            console.log("LDAP vendor changed. Previous=" + $scope.lastVendor + " New=" + $scope.instance.config['vendor'][0]);
             $scope.lastVendor = $scope.instance.config['vendor'][0];
 
             if ($scope.lastVendor === "ad") {
@@ -1771,8 +1788,8 @@ module.controller('LDAPUserStorageCtrl', function($scope, $location, Notificatio
 
     $scope.save = function() {
         $scope.changed = false;
-        if (!parseInt($scope.instance.config['batchSizeForSync'[0]])) {
-            $scope.instance.config['batchSizeForSync'][0] = DEFAULT_BATCH_SIZE;
+        if (!parseInt($scope.instance.config['batchSizeForSync'][0])) {
+            $scope.instance.config['batchSizeForSync'] = [ DEFAULT_BATCH_SIZE ];
         } else {
             $scope.instance.config['batchSizeForSync'][0] = parseInt($scope.instance.config.batchSizeForSync).toString();
         }
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-list.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-list.html
index 84600c0..2574f2d 100644
--- a/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-list.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-list.html
@@ -23,14 +23,14 @@
                             </div>
                             <div class="input-group">
                                 <select class="form-control search" data-ng-model="query.type"
-                                        ng-options="p.type as p.name group by p.group for p in policyProviders track by p.type" data-ng-change="firstPage()">
+                                        ng-options="p.type as p.name for p in policyProviders track by p.type" data-ng-change="firstPage()">
                                     <option value="" selected ng-click="query.type = ''">{{:: 'authz-all-types' | translate}}</option>
                                 </select>
                             </div>
                         </div>
                         <div class="pull-right">
                             <select class="form-control" ng-model="policyType"
-                                    ng-options="p.name group by p.group for p in policyProviders track by p.type"
+                                    ng-options="p.name for p in policyProviders track by p.type"
                                     data-ng-change="addPolicy(policyType);">
                                 <option value="" disabled selected>{{:: 'authz-create-policy' | translate}}...</option>
                             </select>
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/client-scope-mappings.html b/themes/src/main/resources/theme/base/admin/resources/partials/client-scope-mappings.html
index 47ab627..fc10e6b 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/client-scope-mappings.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/client-scope-mappings.html
@@ -41,7 +41,7 @@
             </fieldset>
         </form>
 
-        <form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageClients" data-ng-show="!client.fullScopeAllowed" data-ng-hide="client.useTemplateScope && template && template.fullScopeAllowed">
+        <form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageClients" data-ng-hide="hideRoleSelector()">
             <div class="form-group">
                 <label class="col-md-2 control-label" class="control-label">{{:: 'realm-roles' | translate}}</label>
                 <div class="col-md-10">
@@ -132,6 +132,6 @@
                 </div>
             </div>
         </form>
-    </div>
+        </div>
 
 <kc-menu></kc-menu>
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-ldap.html b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-ldap.html
index 089a65f..a4f17a8 100644
--- a/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-ldap.html
+++ b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-ldap.html
@@ -1,6 +1,6 @@
 <div data-ng-controller="LDAPTabCtrl">
     <h1 data-ng-hide="create">
-        {{instance.displayName|capitalize}}
+        {{instance.name|capitalize}}
         <i class="pficon pficon-delete clickable" data-ng-show="!create && access.manageUsers" data-ng-click="removeUserFederation()"></i>
     </h1>
     <h1 data-ng-show="create">{{:: 'add-user-federation-provider' | translate}}</h1>
diff --git a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml
index 840fa2c..3c8f429 100755
--- a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml
+++ b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml
@@ -92,10 +92,10 @@
         <replacement placeholder="CACHE-CONTAINERS">
             <cache-container name="keycloak" jndi-name="infinispan/Keycloak">
                 <transport lock-timeout="60000"/>
-                <invalidation-cache name="realms" mode="SYNC"/>
-                <invalidation-cache name="users" mode="SYNC">
+                <local-cache name="realms"/>
+                <local-cache name="users">
                     <eviction max-entries="10000" strategy="LRU"/>
-                </invalidation-cache>
+                </local-cache>
                 <distributed-cache name="sessions" mode="SYNC" owners="1"/>
                 <distributed-cache name="offlineSessions" mode="SYNC" owners="1"/>
                 <distributed-cache name="loginFailures" mode="SYNC" owners="1"/>