keycloak-aplcache

Changes

Details

diff --git a/distribution/demo-dist/src/main/xslt/standalone.xsl b/distribution/demo-dist/src/main/xslt/standalone.xsl
index b75b775..882d1b1 100755
--- a/distribution/demo-dist/src/main/xslt/standalone.xsl
+++ b/distribution/demo-dist/src/main/xslt/standalone.xsl
@@ -82,8 +82,12 @@
     <xsl:template match="//*[local-name()='subsystem' and starts-with(namespace-uri(), $inf)]">
         <xsl:copy>
             <cache-container name="keycloak" jndi-name="infinispan/Keycloak">
-                <local-cache name="realms"/>
-                <local-cache name="users"/>
+                <local-cache name="realms">
+                    <eviction max-entries="10000" strategy="LRU"/>
+                </local-cache>
+                <local-cache name="users">
+                    <eviction max-entries="10000" strategy="LRU"/>
+                </local-cache>
                 <local-cache name="sessions"/>
                 <local-cache name="offlineSessions"/>
                 <local-cache name="loginFailures"/>
diff --git a/distribution/server-overlay/src/main/cli/keycloak-install.cli b/distribution/server-overlay/src/main/cli/keycloak-install.cli
index d62bcf1..835320d 100644
--- a/distribution/server-overlay/src/main/cli/keycloak-install.cli
+++ b/distribution/server-overlay/src/main/cli/keycloak-install.cli
@@ -2,6 +2,7 @@ embed-server --server-config=standalone.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/local-cache=realms:add()
+/subsystem=infinispan/cache-container=keycloak/local-cache=realms/eviction=EVICTION:add(max-entries=10000,strategy=LRU)
 /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/local-cache=sessions:add()
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 a3b85f1..b7f2631 100644
--- a/distribution/server-overlay/src/main/cli/keycloak-install-ha.cli
+++ b/distribution/server-overlay/src/main/cli/keycloak-install-ha.cli
@@ -3,6 +3,7 @@ embed-server --server-config=standalone-ha.xml
 /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/local-cache=realms:add()
+/subsystem=infinispan/cache-container=keycloak/local-cache=realms/eviction=EVICTION:add(max-entries=10000,strategy=LRU)
 /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")
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProvider.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProvider.java
index c772d36..d1ae1f8 100755
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProvider.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProvider.java
@@ -421,7 +421,12 @@ public class LDAPStorageProvider implements UserStorageProvider,
 
         // Check here if user already exists
         String ldapUsername = LDAPUtils.getUsername(ldapUser, ldapIdentityStore.getConfig());
-        if (session.userLocalStorage().getUserByUsername(ldapUsername, realm) != null) {
+        UserModel user = session.userLocalStorage().getUserByUsername(ldapUsername, realm);
+        
+        if (user != null) {
+            LDAPUtils.checkUuid(ldapUser, ldapIdentityStore.getConfig());
+            // If email attribute mapper is set to "Always Read Value From LDAP" the user may be in Keycloak DB with an old email address
+            if (ldapUser.getUuid().equals(user.getFirstAttribute(LDAPConstants.LDAP_ID))) return user;
             throw new ModelDuplicateException("User with username '" + ldapUsername + "' already exists in Keycloak. It conflicts with LDAP user with email '" + email + "'");
         }
 
@@ -483,9 +488,20 @@ public class LDAPStorageProvider implements UserStorageProvider,
             UserCredentialModel cred = (UserCredentialModel)input;
             String password = cred.getValue();
             LDAPObject ldapUser = loadAndValidateUser(realm, user);
-            ldapIdentityStore.updatePassword(ldapUser, password);
-            if (updater != null) updater.passwordUpdated(user, ldapUser, input);
-            return true;
+
+            try {
+                ldapIdentityStore.updatePassword(ldapUser, password);
+                if (updater != null) updater.passwordUpdated(user, ldapUser, input);
+                return true;
+            } catch (ModelException me) {
+                if (updater != null) {
+                    updater.passwordUpdateFailed(user, ldapUser, input, me);
+                    return false;
+                } else {
+                    throw me;
+                }
+            }
+
         } else {
             return false;
         }
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/msad/MSADUserAccountControlStorageMapper.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/msad/MSADUserAccountControlStorageMapper.java
index 2a82c04..1614fef 100644
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/msad/MSADUserAccountControlStorageMapper.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/msad/MSADUserAccountControlStorageMapper.java
@@ -90,6 +90,11 @@ public class MSADUserAccountControlStorageMapper extends AbstractLDAPStorageMapp
     }
 
     @Override
+    public void passwordUpdateFailed(UserModel user, LDAPObject ldapUser, CredentialInput input, ModelException exception) {
+        throw processFailedPasswordUpdateException(exception);
+    }
+
+    @Override
     public UserModel proxy(LDAPObject ldapUser, UserModel delegate) {
         return new MSADUserModelDelegate(delegate, ldapUser);
     }
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/msadlds/MSADLDSUserAccountControlStorageMapper.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/msadlds/MSADLDSUserAccountControlStorageMapper.java
index be07781..301ad3a 100644
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/msadlds/MSADLDSUserAccountControlStorageMapper.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/msadlds/MSADLDSUserAccountControlStorageMapper.java
@@ -89,6 +89,11 @@ public class MSADLDSUserAccountControlStorageMapper extends AbstractLDAPStorageM
     }
 
     @Override
+    public void passwordUpdateFailed(UserModel user, LDAPObject ldapUser, CredentialInput input, ModelException exception) {
+        throw processFailedPasswordUpdateException(exception);
+    }
+
+    @Override
     public UserModel proxy(LDAPObject ldapUser, UserModel delegate) {
         return new MSADUserModelDelegate(delegate, ldapUser);
     }
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/PasswordUpdated.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/PasswordUpdated.java
index c4d7b5e..a2f255a 100644
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/PasswordUpdated.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/PasswordUpdated.java
@@ -17,6 +17,7 @@
 package org.keycloak.storage.ldap.mappers;
 
 import org.keycloak.credential.CredentialInput;
+import org.keycloak.models.ModelException;
 import org.keycloak.models.UserModel;
 import org.keycloak.storage.ldap.idm.model.LDAPObject;
 
@@ -25,5 +26,8 @@ import org.keycloak.storage.ldap.idm.model.LDAPObject;
  * @version $Revision: 1 $
  */
 public interface PasswordUpdated {
+
     void passwordUpdated(UserModel user, LDAPObject ldapUser, CredentialInput input);
+
+    void passwordUpdateFailed(UserModel user, LDAPObject ldapUser, CredentialInput input, ModelException exception) throws ModelException;
 }
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 7781e3a..f9a3070 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
@@ -103,15 +103,20 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
             cacheManager = (EmbeddedCacheManager) new InitialContext().lookup(cacheContainerLookup);
             containerManaged = true;
 
-            cacheManager.defineConfiguration(InfinispanConnectionProvider.REALM_REVISIONS_CACHE_NAME, getRevisionCacheConfig(InfinispanConnectionProvider.REALM_REVISIONS_CACHE_DEFAULT_MAX));
-            cacheManager.getCache(InfinispanConnectionProvider.REALM_CACHE_NAME, true);
+            long realmRevisionsMaxEntries = cacheManager.getCache(InfinispanConnectionProvider.REALM_CACHE_NAME).getCacheConfiguration().eviction().maxEntries();
+            realmRevisionsMaxEntries = realmRevisionsMaxEntries > 0
+                    ? 2 * realmRevisionsMaxEntries
+                    : InfinispanConnectionProvider.REALM_REVISIONS_CACHE_DEFAULT_MAX;
 
-            long maxEntries = cacheManager.getCache(InfinispanConnectionProvider.USER_CACHE_NAME).getCacheConfiguration().eviction().maxEntries();
-            if (maxEntries <= 0) {
-                maxEntries = InfinispanConnectionProvider.USER_REVISIONS_CACHE_DEFAULT_MAX;
-            }
+            cacheManager.defineConfiguration(InfinispanConnectionProvider.REALM_REVISIONS_CACHE_NAME, getRevisionCacheConfig(realmRevisionsMaxEntries));
+            cacheManager.getCache(InfinispanConnectionProvider.REALM_REVISIONS_CACHE_NAME, true);
+
+            long userRevisionsMaxEntries = cacheManager.getCache(InfinispanConnectionProvider.USER_CACHE_NAME).getCacheConfiguration().eviction().maxEntries();
+            userRevisionsMaxEntries = userRevisionsMaxEntries > 0
+                    ? 2 * userRevisionsMaxEntries
+                    : InfinispanConnectionProvider.USER_REVISIONS_CACHE_DEFAULT_MAX;
 
-            cacheManager.defineConfiguration(InfinispanConnectionProvider.USER_REVISIONS_CACHE_NAME, getRevisionCacheConfig(maxEntries));
+            cacheManager.defineConfiguration(InfinispanConnectionProvider.USER_REVISIONS_CACHE_NAME, getRevisionCacheConfig(userRevisionsMaxEntries));
             cacheManager.getCache(InfinispanConnectionProvider.USER_REVISIONS_CACHE_NAME, true);
             cacheManager.getCache(InfinispanConnectionProvider.AUTHORIZATION_CACHE_NAME, true);
             cacheManager.getCache(InfinispanConnectionProvider.KEYS_CACHE_NAME, true);
@@ -189,15 +194,20 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
         counterConfigBuilder.transaction().transactionManagerLookup(new DummyTransactionManagerLookup());
         counterConfigBuilder.transaction().lockingMode(LockingMode.PESSIMISTIC);
 
-        cacheManager.defineConfiguration(InfinispanConnectionProvider.REALM_REVISIONS_CACHE_NAME, getRevisionCacheConfig(InfinispanConnectionProvider.REALM_REVISIONS_CACHE_DEFAULT_MAX));
-        cacheManager.getCache(InfinispanConnectionProvider.REALM_CACHE_NAME, true);
+        long realmRevisionsMaxEntries = cacheManager.getCache(InfinispanConnectionProvider.REALM_CACHE_NAME).getCacheConfiguration().eviction().maxEntries();
+        realmRevisionsMaxEntries = realmRevisionsMaxEntries > 0
+                ? 2 * realmRevisionsMaxEntries
+                : InfinispanConnectionProvider.REALM_REVISIONS_CACHE_DEFAULT_MAX;
 
-        long maxEntries = cacheManager.getCache(InfinispanConnectionProvider.USER_CACHE_NAME).getCacheConfiguration().eviction().maxEntries();
-        if (maxEntries <= 0) {
-            maxEntries = InfinispanConnectionProvider.USER_REVISIONS_CACHE_DEFAULT_MAX;
-        }
+        cacheManager.defineConfiguration(InfinispanConnectionProvider.REALM_REVISIONS_CACHE_NAME, getRevisionCacheConfig(realmRevisionsMaxEntries));
+        cacheManager.getCache(InfinispanConnectionProvider.REALM_REVISIONS_CACHE_NAME, true);
+
+        long userRevisionsMaxEntries = cacheManager.getCache(InfinispanConnectionProvider.USER_CACHE_NAME).getCacheConfiguration().eviction().maxEntries();
+        userRevisionsMaxEntries = userRevisionsMaxEntries > 0
+                ? 2 * userRevisionsMaxEntries
+                : InfinispanConnectionProvider.USER_REVISIONS_CACHE_DEFAULT_MAX;
 
-        cacheManager.defineConfiguration(InfinispanConnectionProvider.USER_REVISIONS_CACHE_NAME, getRevisionCacheConfig(maxEntries));
+        cacheManager.defineConfiguration(InfinispanConnectionProvider.USER_REVISIONS_CACHE_NAME, getRevisionCacheConfig(userRevisionsMaxEntries));
         cacheManager.getCache(InfinispanConnectionProvider.USER_REVISIONS_CACHE_NAME, true);
 
         cacheManager.defineConfiguration(InfinispanConnectionProvider.KEYS_CACHE_NAME, getKeysCacheConfig());
diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java
index 5173c3c..7c255fd 100755
--- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java
+++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java
@@ -27,7 +27,7 @@ public interface InfinispanConnectionProvider extends Provider {
 
     String REALM_CACHE_NAME = "realms";
     String REALM_REVISIONS_CACHE_NAME = "realmRevisions";
-    int REALM_REVISIONS_CACHE_DEFAULT_MAX = 10000;
+    int REALM_REVISIONS_CACHE_DEFAULT_MAX = 20000;
 
     String USER_CACHE_NAME = "users";
     String USER_REVISIONS_CACHE_NAME = "userRevisions";
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 b6862f5..41cb41d 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
@@ -38,6 +38,7 @@ public class RoleAdapter implements RoleModel {
     protected CachedRole cached;
     protected RealmCacheSession cacheSession;
     protected RealmModel realm;
+    protected Set<RoleModel> composites;
 
     public RoleAdapter(CachedRole cached, RealmCacheSession session, RealmModel realm) {
         this.cached = cached;
@@ -132,15 +133,19 @@ public class RoleAdapter implements RoleModel {
     @Override
     public Set<RoleModel> getComposites() {
         if (isUpdated()) return updated.getComposites();
-        Set<RoleModel> set = new HashSet<RoleModel>();
-        for (String id : cached.getComposites()) {
-            RoleModel role = realm.getRoleById(id);
-            if (role == null) {
-                throw new IllegalStateException("Could not find composite in role " + getName() + ": " + id);
+
+        if (composites == null) {
+            composites = new HashSet<RoleModel>();
+            for (String id : cached.getComposites()) {
+                RoleModel role = realm.getRoleById(id);
+                if (role == null) {
+                    throw new IllegalStateException("Could not find composite in role " + getName() + ": " + id);
+                }
+                composites.add(role);
             }
-            set.add(role);
         }
-        return set;
+
+        return composites;
     }
 
     @Override
@@ -171,11 +176,7 @@ public class RoleAdapter implements RoleModel {
 
     @Override
     public boolean hasRole(RoleModel role) {
-        if (this.equals(role)) return true;
-        if (!isComposite()) return false;
-
-        Set<RoleModel> visited = new HashSet<RoleModel>();
-        return KeycloakModelUtils.searchFor(role, this, visited);
+        return this.equals(role) || KeycloakModelUtils.searchFor(role, this);
     }
 
     @Override
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RoleAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RoleAdapter.java
index 3cc1553..10e6252 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/RoleAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RoleAdapter.java
@@ -128,11 +128,7 @@ public class RoleAdapter implements RoleModel, JpaModel<RoleEntity> {
 
     @Override
     public boolean hasRole(RoleModel role) {
-        if (this.equals(role)) return true;
-        if (!isComposite()) return false;
-
-        Set<RoleModel> visited = new HashSet<RoleModel>();
-        return KeycloakModelUtils.searchFor(role, this, visited);
+        return this.equals(role) || KeycloakModelUtils.searchFor(role, this);
     }
 
     @Override
diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-2.4.1.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-2.4.1.xml
index 8c14b67..543d8b6 100755
--- a/model/jpa/src/main/resources/META-INF/jpa-changelog-2.4.1.xml
+++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-2.4.1.xml
@@ -50,7 +50,7 @@
         </addColumn>
         <sql>UPDATE COMPONENT_CONFIG SET VALUE_NEW = VALUE, VALUE = NULL</sql>
         <dropColumn tableName="COMPONENT_CONFIG" columnName="VALUE"/>
-        <renameColumn tableName="COMPONENT_CONFIG" oldColumnName="VALUE_NEW" newColumnName="VALUE"/>
+        <renameColumn tableName="COMPONENT_CONFIG" oldColumnName="VALUE_NEW" newColumnName="VALUE" columnDataType="NCLOB"/>
 <!--
         <modifyDataType tableName="COMPONENT_CONFIG" columnName="VALUE" newDataType="NVARCHAR(2000)"/>
 -->
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RoleAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RoleAdapter.java
index 47ca980..e141ff8 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RoleAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RoleAdapter.java
@@ -172,11 +172,7 @@ public class RoleAdapter extends AbstractMongoAdapter<MongoRoleEntity> implement
 
     @Override
     public boolean hasRole(RoleModel role) {
-        if (this.equals(role)) return true;
-        if (!isComposite()) return false;
-
-        Set<RoleModel> visited = new HashSet<RoleModel>();
-        return KeycloakModelUtils.searchFor(role, this, visited);
+        return this.equals(role) || KeycloakModelUtils.searchFor(role, this);
     }
 
     public MongoRoleEntity getRole() {
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/util/SAMLParserUtil.java b/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/util/SAMLParserUtil.java
index 8b17822..c2337b2 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/util/SAMLParserUtil.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/util/SAMLParserUtil.java
@@ -320,6 +320,8 @@ public class SAMLParserUtil {
             return StaxParserUtil.getElementText(xmlEventReader);
         } else if(typeValue.contains(":base64Binary")){
             return StaxParserUtil.getElementText(xmlEventReader);
+        } else if(typeValue.contains(":boolean")){
+            return StaxParserUtil.getElementText(xmlEventReader);
         }
 
 
diff --git a/server-spi/src/main/java/org/keycloak/models/utils/RoleUtils.java b/server-spi/src/main/java/org/keycloak/models/utils/RoleUtils.java
index a03a55d..1dd7267 100644
--- a/server-spi/src/main/java/org/keycloak/models/utils/RoleUtils.java
+++ b/server-spi/src/main/java/org/keycloak/models/utils/RoleUtils.java
@@ -20,7 +20,11 @@ package org.keycloak.models.utils;
 import org.keycloak.models.GroupModel;
 import org.keycloak.models.RoleModel;
 
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.HashSet;
 import java.util.Set;
+import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
 
 /**
@@ -98,4 +102,33 @@ public class RoleUtils {
                 .anyMatch(group -> hasRoleFromGroup(group, targetRole, checkParentGroup));
     }
 
+    /**
+     * Recursively expands composite roles into their composite.
+     * @param role
+     * @return Stream of containing all of the composite roles and their components.
+     */
+    public static Stream<RoleModel> expandCompositeRolesStream(RoleModel role) {
+        Stream.Builder<RoleModel> sb = Stream.builder();
+        Set<RoleModel> roles = new HashSet<>();
+
+        Deque<RoleModel> stack = new ArrayDeque<>();
+        stack.add(role);
+
+        while (! stack.isEmpty()) {
+            RoleModel current = stack.pop();
+            sb.add(current);
+
+            if (current.isComposite()) {
+                current.getComposites().stream()
+                  .filter(r -> ! roles.contains(r))
+                  .forEach(r -> {
+                    roles.add(r);
+                    stack.add(r);
+                  });
+            }
+        }
+
+        return sb.build();
+    }
+
 }
diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java b/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
index c0f28db..cba151e 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
@@ -177,16 +177,14 @@ public final class KeycloakModelUtils {
      * @param visited   set of already visited roles (used for recursion)
      * @return true if "role" is descendant of "composite"
      */
-    public static boolean searchFor(RoleModel role, RoleModel composite, Set<RoleModel> visited) {
-        if (visited.contains(composite)) return false;
-        visited.add(composite);
-        Set<RoleModel> composites = composite.getComposites();
-        if (composites.contains(role)) return true;
-        for (RoleModel contained : composites) {
-            if (!contained.isComposite()) continue;
-            if (searchFor(role, contained, visited)) return true;
-        }
-        return false;
+    public static boolean searchFor(RoleModel role, RoleModel composite) {
+        return composite.isComposite() && (
+                composite.getComposites().contains(role) ||
+                        composite.getComposites().stream()
+                                .filter(x -> x.isComposite() && x.hasRole(role))
+                                .findFirst()
+                                .isPresent()
+        );
     }
 
     /**
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticator.java
index 9bff3f9..6ab2907 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticator.java
@@ -52,6 +52,10 @@ import java.util.Map;
  * </ol>
  * </p>
  * <p>
+ * Note that the {@code user} variable is only defined when the user was identified by a preceeding
+ * authentication step, e.g. by the {@link UsernamePasswordForm} authenticator.
+ * </p>
+ * <p>
  * Additional context information can be extracted from the {@code context} argument passed to the {@code authenticate(context)}
  * or {@code action(context)} function.
  * <p>
@@ -63,9 +67,10 @@ import java.util.Map;
  *
  *   function authenticate(context) {
  *
- *     LOG.info(script.name + " --> trace auth for: " + user.username);
+ *     var username = user ? user.username : "anonymous";
+ *     LOG.info(script.name + " --> trace auth for: " + username);
  *
- *     if (   user.username === "tester"
+ *     if (   username === "tester"
  *         && user.getAttribute("someAttribute")
  *         && user.getAttribute("someAttribute").contains("someValue")) {
  *
@@ -160,7 +165,7 @@ public class ScriptBasedAuthenticator implements Authenticator {
 
     @Override
     public boolean requiresUser() {
-        return true;
+        return false;
     }
 
     @Override
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 4de3720..bfedd29 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
@@ -22,10 +22,9 @@ import org.keycloak.models.ProtocolMapperModel;
 import org.keycloak.models.RoleModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.utils.RoleUtils;
 import org.keycloak.representations.IDToken;
 
-import java.util.ArrayDeque;
-import java.util.Deque;
 import java.util.Set;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
@@ -54,7 +53,7 @@ abstract class AbstractUserRoleMappingMapper extends AbstractOIDCProtocolMapper 
           user.getGroups().stream()
             .flatMap(g -> groupAndItsParentsStream(g))
             .flatMap(g -> g.getRoleMappings().stream()))
-          .flatMap(role -> expandCompositeRolesStream(role));
+          .flatMap(RoleUtils::expandCompositeRolesStream);
     }
 
     /**
@@ -72,29 +71,6 @@ abstract class AbstractUserRoleMappingMapper extends AbstractOIDCProtocolMapper 
     }
 
     /**
-     * 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<>();
-        stack.add(role);
-
-        while (! stack.isEmpty()) {
-            RoleModel current = stack.pop();
-            sb.add(current);
-
-            if (current.isComposite()) {
-                stack.addAll(current.getComposites());
-            }
-        }
-
-        return sb.build();
-    }
-
-    /**
      * 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),
diff --git a/services/src/main/java/org/keycloak/protocol/saml/mappers/HardcodedRole.java b/services/src/main/java/org/keycloak/protocol/saml/mappers/HardcodedRole.java
index 9efead2..9b78e60 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/mappers/HardcodedRole.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/mappers/HardcodedRole.java
@@ -35,12 +35,13 @@ import java.util.Map;
 public class HardcodedRole extends AbstractSAMLProtocolMapper {
     public static final String PROVIDER_ID = "saml-hardcode-role-mapper";
     public static final String ATTRIBUTE_VALUE = "attribute.value";
-    private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
+    private static final List<ProviderConfigProperty> configProperties = new ArrayList<>();
+    public static final String ROLE_ATTRIBUTE = "role";
 
     static {
         ProviderConfigProperty property;
         property = new ProviderConfigProperty();
-        property.setName("role");
+        property.setName(ROLE_ATTRIBUTE);
         property.setLabel("Role");
         property.setHelpText("Arbitrary role name you want to hardcode.  This role does not have to exist in current realm and can be just any string you need");
         property.setType(ProviderConfigProperty.ROLE_TYPE);
@@ -79,8 +80,8 @@ public class HardcodedRole extends AbstractSAMLProtocolMapper {
         mapper.setName(name);
         mapper.setProtocolMapper(mapperId);
         mapper.setProtocol(SamlProtocol.LOGIN_PROTOCOL);
-       Map<String, String> config = new HashMap<String, String>();
-        config.put("role", role);
+        Map<String, String> config = new HashMap<>();
+        config.put(ROLE_ATTRIBUTE, role);
         mapper.setConfig(config);
         return mapper;
 
diff --git a/services/src/main/java/org/keycloak/protocol/saml/mappers/RoleListMapper.java b/services/src/main/java/org/keycloak/protocol/saml/mappers/RoleListMapper.java
index dd27472..5650333 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/mappers/RoleListMapper.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/mappers/RoleListMapper.java
@@ -23,8 +23,9 @@ import org.keycloak.models.ClientSessionModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
 import org.keycloak.models.ProtocolMapperModel;
-import org.keycloak.models.RoleModel;
+import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.utils.RoleUtils;
 import org.keycloak.protocol.ProtocolMapper;
 import org.keycloak.protocol.saml.SamlProtocol;
 import org.keycloak.provider.ProviderConfigProperty;
@@ -35,7 +36,9 @@ import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 /**
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -45,7 +48,7 @@ public class RoleListMapper extends AbstractSAMLProtocolMapper implements SAMLRo
     public static final String PROVIDER_ID = "saml-role-list-mapper";
     public static final String SINGLE_ROLE_ATTRIBUTE = "single";
 
-    private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
+    private static final List<ProviderConfigProperty> configProperties = new ArrayList<>();
 
     static {
         ProviderConfigProperty property;
@@ -120,11 +123,13 @@ public class RoleListMapper extends AbstractSAMLProtocolMapper implements SAMLRo
 
             ProtocolMapper mapper = (ProtocolMapper)sessionFactory.getProviderFactory(ProtocolMapper.class, mapping.getProtocolMapper());
             if (mapper == null) continue;
+
             if (mapper instanceof SAMLRoleNameMapper) {
                 roleNameMappers.add(new SamlProtocol.ProtocolMapperProcessor<>((SAMLRoleNameMapper) mapper,mapping));
             }
+
             if (mapper instanceof HardcodedRole) {
-                AttributeType attributeType = null;
+                AttributeType attributeType;
                 if (singleAttribute) {
                     if (singleAttributeType == null) {
                         singleAttributeType = AttributeStatementHelper.createAttributeType(mappingModel);
@@ -135,14 +140,26 @@ public class RoleListMapper extends AbstractSAMLProtocolMapper implements SAMLRo
                     attributeType = AttributeStatementHelper.createAttributeType(mappingModel);
                     roleAttributeStatement.addAttribute(new AttributeStatementType.ASTChoiceType(attributeType));
                 }
-                attributeType.addAttributeValue(mapping.getConfig().get("role"));
+
+                attributeType.addAttributeValue(mapping.getConfig().get(HardcodedRole.ROLE_ATTRIBUTE));
             }
         }
 
-        for (String roleId : clientSession.getRoles()) {
-            // todo need a role mapping
-            RoleModel roleModel = clientSession.getRealm().getRoleById(roleId);
-            AttributeType attributeType = null;
+        RealmModel realm = clientSession.getRealm();
+        List<String> allRoleNames = clientSession.getRoles().stream()
+          // todo need a role mapping
+          .map(realm::getRoleById)
+          .filter(Objects::nonNull)
+          .flatMap(RoleUtils::expandCompositeRolesStream)
+          .map(roleModel -> roleNameMappers.stream()
+            .map(entry -> entry.mapper.mapName(entry.model, roleModel))
+            .filter(Objects::nonNull)
+            .findFirst()
+            .orElse(roleModel.getName())
+          ).collect(Collectors.toList());
+
+        for (String roleName : allRoleNames) {
+            AttributeType attributeType;
             if (singleAttribute) {
                 if (singleAttributeType == null) {
                     singleAttributeType = AttributeStatementHelper.createAttributeType(mappingModel);
@@ -153,14 +170,7 @@ public class RoleListMapper extends AbstractSAMLProtocolMapper implements SAMLRo
                 attributeType = AttributeStatementHelper.createAttributeType(mappingModel);
                 roleAttributeStatement.addAttribute(new AttributeStatementType.ASTChoiceType(attributeType));
             }
-            String roleName = roleModel.getName();
-            for (SamlProtocol.ProtocolMapperProcessor<SAMLRoleNameMapper> entry : roleNameMappers) {
-                String newName = entry.mapper.mapName(entry.model, roleModel);
-                if (newName != null) {
-                    roleName = newName;
-                    break;
-                }
-            }
+
             attributeType.addAttributeValue(roleName);
         }
 
@@ -172,7 +182,7 @@ public class RoleListMapper extends AbstractSAMLProtocolMapper implements SAMLRo
         mapper.setProtocolMapper(PROVIDER_ID);
         mapper.setProtocol(SamlProtocol.LOGIN_PROTOCOL);
         mapper.setConsentRequired(false);
-        Map<String, String> config = new HashMap<String, String>();
+        Map<String, String> config = new HashMap<>();
         config.put(AttributeStatementHelper.SAML_ATTRIBUTE_NAME, samlAttributeName);
         if (friendlyName != null) {
             config.put(AttributeStatementHelper.FRIENDLY_NAME, friendlyName);
diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlService.java b/services/src/main/java/org/keycloak/protocol/saml/SamlService.java
index c404ef8..b69e57e 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/SamlService.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/SamlService.java
@@ -248,9 +248,9 @@ public class SamlService extends AuthorizationEndpointBase {
             String bindingType = getBindingType(requestAbstractType);
             if (samlClient.forcePostBinding())
                 bindingType = SamlProtocol.SAML_POST_BINDING;
-            String redirect = null;
+            String redirect;
             URI redirectUri = requestAbstractType.getAssertionConsumerServiceURL();
-            if (redirectUri != null && !"null".equals(redirectUri)) { // "null" is for testing purposes
+            if (redirectUri != null && ! "null".equals(redirectUri.toString())) { // "null" is for testing purposes
                 redirect = RedirectUtils.verifyRedirectUri(uriInfo, redirectUri.toString(), realm, client);
             } else {
                 if (bindingType.equals(SamlProtocol.SAML_POST_BINDING)) {
@@ -279,8 +279,9 @@ public class SamlService extends AuthorizationEndpointBase {
 
             // Handle NameIDPolicy from SP
             NameIDPolicyType nameIdPolicy = requestAbstractType.getNameIDPolicy();
-            if (nameIdPolicy != null && !samlClient.forceNameIDFormat()) {
-                String nameIdFormat = nameIdPolicy.getFormat().toString();
+            final URI nameIdFormatUri = nameIdPolicy == null ? null : nameIdPolicy.getFormat();
+            if (nameIdFormatUri != null && ! samlClient.forceNameIDFormat()) {
+                String nameIdFormat = nameIdFormatUri.toString();
                 // TODO: Handle AllowCreate too, relevant for persistent NameID.
                 if (isSupportedNameIdFormat(nameIdFormat)) {
                     clientSession.setNote(GeneralConstants.NAMEID_FORMAT, nameIdFormat);
@@ -345,7 +346,7 @@ public class SamlService extends AuthorizationEndpointBase {
             AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(session, realm, false);
             if (authResult != null) {
                 String logoutBinding = getBindingType();
-                if ("true".equals(samlClient.forcePostBinding()))
+                if (samlClient.forcePostBinding())
                     logoutBinding = SamlProtocol.SAML_POST_BINDING;
                 boolean postBinding = Objects.equals(SamlProtocol.SAML_POST_BINDING, logoutBinding);
 
diff --git a/services/src/main/resources/scripts/authenticator-template.js b/services/src/main/resources/scripts/authenticator-template.js
index 73bb124..20de702 100644
--- a/services/src/main/resources/scripts/authenticator-template.js
+++ b/services/src/main/resources/scripts/authenticator-template.js
@@ -24,7 +24,8 @@ AuthenticationFlowError = Java.type("org.keycloak.authentication.AuthenticationF
  */
 function authenticate(context) {
 
-    LOG.info(script.name + " trace auth for: " + user.username);
+    var username = user ? user.username : "anonymous";
+    LOG.info(script.name + " trace auth for: " + username);
 
     var authShouldFail = false;
     if (authShouldFail) {
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/CompositeRolesModelTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/CompositeRolesModelTest.java
index 038c148..78ba714 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/CompositeRolesModelTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/CompositeRolesModelTest.java
@@ -66,18 +66,27 @@ public class CompositeRolesModelTest extends AbstractModelTest {
     @Test
     public void testComposites() {
         Set<RoleModel> requestedRoles = getRequestedRoles("APP_COMPOSITE_APPLICATION", "APP_COMPOSITE_USER");
-        Assert.assertEquals(2, requestedRoles.size());
+        Assert.assertEquals(5, requestedRoles.size());
+        assertContains("APP_COMPOSITE_APPLICATION", "APP_COMPOSITE_ROLE", requestedRoles);
+        assertContains("APP_COMPOSITE_APPLICATION", "APP_COMPOSITE_CHILD", requestedRoles);
+        assertContains("APP_COMPOSITE_APPLICATION", "APP_ROLE_2", requestedRoles);
         assertContains("APP_ROLE_APPLICATION", "APP_ROLE_1", requestedRoles);
         assertContains("realm", "REALM_ROLE_1", requestedRoles);
 
         requestedRoles = getRequestedRoles("APP_COMPOSITE_APPLICATION", "REALM_APP_COMPOSITE_USER");
-        Assert.assertEquals(1, requestedRoles.size());
+        Assert.assertEquals(4, requestedRoles.size());
         assertContains("APP_ROLE_APPLICATION", "APP_ROLE_1", requestedRoles);
 
         requestedRoles = getRequestedRoles("REALM_COMPOSITE_1_APPLICATION", "REALM_COMPOSITE_1_USER");
         Assert.assertEquals(1, requestedRoles.size());
         assertContains("realm", "REALM_COMPOSITE_1", requestedRoles);
 
+        requestedRoles = getRequestedRoles("REALM_COMPOSITE_2_APPLICATION", "REALM_COMPOSITE_1_USER");
+        Assert.assertEquals(3, requestedRoles.size());
+        assertContains("realm", "REALM_COMPOSITE_1", requestedRoles);
+        assertContains("realm", "REALM_COMPOSITE_CHILD", requestedRoles);
+        assertContains("realm", "REALM_ROLE_4", requestedRoles);
+
         requestedRoles = getRequestedRoles("REALM_ROLE_1_APPLICATION", "REALM_COMPOSITE_1_USER");
         Assert.assertEquals(1, requestedRoles.size());
         assertContains("realm", "REALM_ROLE_1", requestedRoles);
diff --git a/testsuite/integration/src/test/resources/model/testcomposites.json b/testsuite/integration/src/test/resources/model/testcomposites.json
index 740a4e1..d9e9bb1 100755
--- a/testsuite/integration/src/test/resources/model/testcomposites.json
+++ b/testsuite/integration/src/test/resources/model/testcomposites.json
@@ -21,7 +21,7 @@
             "email" : "test-user1@localhost",
             "credentials" : [
                 { "type" : "password",
-                  "value" : "password" }
+                    "value" : "password" }
             ],
             "realmRoles": [ "REALM_COMPOSITE_1" ]
         },
@@ -31,7 +31,7 @@
             "email" : "test-user2@localhost",
             "credentials" : [
                 { "type" : "password",
-                  "value" : "password" }
+                    "value" : "password" }
             ],
             "realmRoles": [ "REALM_ROLE_1"]
         },
@@ -41,7 +41,7 @@
             "email" : "test-user3@localhost",
             "credentials" : [
                 { "type" : "password",
-                  "value" : "password" }
+                    "value" : "password" }
             ],
             "realmRoles": [ "REALM_APP_COMPOSITE_ROLE" ]
         },
@@ -51,7 +51,7 @@
             "email" : "test-user4@localhost",
             "credentials" : [
                 { "type" : "password",
-                  "value" : "password" }
+                    "value" : "password" }
             ],
             "applicationRoles": {
                 "APP_ROLE_APPLICATION": [ "APP_ROLE_2" ]
@@ -63,7 +63,7 @@
             "email" : "test-user5@localhost",
             "credentials" : [
                 { "type" : "password",
-                  "value" : "password" }
+                    "value" : "password" }
             ],
             "realmRoles": ["REALM_APP_COMPOSITE_ROLE", "REALM_COMPOSITE_1"]
         }
@@ -81,6 +81,10 @@
             "roles": ["REALM_COMPOSITE_1"]
         },
         {
+            "client": "REALM_COMPOSITE_2_APPLICATION",
+            "roles": ["REALM_COMPOSITE_1", "REALM_COMPOSITE_CHILD", "REALM_ROLE_4"]
+        },
+        {
             "client": "REALM_ROLE_1_APPLICATION",
             "roles": ["REALM_ROLE_1"]
         }
@@ -93,7 +97,15 @@
             "baseUrl": "http://localhost:8081/app",
             "adminUrl": "http://localhost:8081/app/logout",
             "secret": "password"
-         },
+        },
+        {
+            "name": "REALM_COMPOSITE_2_APPLICATION",
+            "fullScopeAllowed": false,
+            "enabled": true,
+            "baseUrl": "http://localhost:8081/app",
+            "adminUrl": "http://localhost:8081/app/logout",
+            "secret": "password"
+        },
         {
             "name": "REALM_ROLE_1_APPLICATION",
             "fullScopeAllowed": false,
@@ -131,9 +143,18 @@
                 "name": "REALM_ROLE_3"
             },
             {
+                "name": "REALM_ROLE_4"
+            },
+            {
                 "name": "REALM_COMPOSITE_1",
                 "composites": {
-                    "realm": ["REALM_ROLE_1"]
+                    "realm": ["REALM_ROLE_1", "REALM_COMPOSITE_CHILD"]
+                }
+            },
+            {
+                "name": "REALM_COMPOSITE_CHILD",
+                "composites": {
+                    "realm": ["REALM_ROLE_4"]
                 }
             },
             {
@@ -142,6 +163,9 @@
                     "application": {
                         "APP_ROLE_APPLICATION" :[
                             "APP_ROLE_1"
+                        ],
+                        "APP_COMPOSITE_APPLICATION" :[
+                            "APP_COMPOSITE_ROLE"
                         ]
                     }
                 }
@@ -168,6 +192,19 @@
                         "application": {
                             "APP_ROLE_APPLICATION" :[
                                 "APP_ROLE_1"
+                            ],
+                            "APP_COMPOSITE_APPLICATION" :[
+                                "APP_COMPOSITE_CHILD"
+                            ]
+                        }
+                    }
+                },
+                {
+                    "name": "APP_COMPOSITE_CHILD",
+                    "composites": {
+                        "application": {
+                            "APP_COMPOSITE_APPLICATION" :[
+                                "APP_ROLE_2"
                             ]
                         }
                     }
@@ -184,7 +221,7 @@
         "APP_ROLE_APPLICATION": [
             {
                 "client": "APP_COMPOSITE_APPLICATION",
-                "roles": ["APP_ROLE_2"]
+                "roles": ["APP_ROLE_1"]
             }
         ]
     }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/AbstractHawtioAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/AbstractHawtioAdapterTest.java
index d5e1da1..d27fbd7 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/AbstractHawtioAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/AbstractHawtioAdapterTest.java
@@ -7,7 +7,6 @@ import org.keycloak.representations.idm.RealmRepresentation;
 import org.keycloak.testsuite.adapter.AbstractExampleAdapterTest;
 import org.keycloak.testsuite.adapter.page.HawtioPage;
 
-import java.io.File;
 import java.util.List;
 
 import static org.keycloak.testsuite.auth.page.AuthRealm.DEMO;
@@ -17,7 +16,7 @@ import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith;
 /**
  * @author mhajas
  */
-public class AbstractHawtioAdapterTest extends AbstractExampleAdapterTest {
+public abstract class AbstractHawtioAdapterTest extends AbstractExampleAdapterTest {
 
     @Page
     private HawtioPage hawtioPage;
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractOIDCPublicKeyRotationAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractOIDCPublicKeyRotationAdapterTest.java
index 8c42b99..c1d5b57 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractOIDCPublicKeyRotationAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractOIDCPublicKeyRotationAdapterTest.java
@@ -72,7 +72,7 @@ import static org.keycloak.testsuite.util.WaitUtils.waitUntilElement;
  *
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
  */
-public class AbstractOIDCPublicKeyRotationAdapterTest extends AbstractServletsAdapterTest {
+public abstract class AbstractOIDCPublicKeyRotationAdapterTest extends AbstractServletsAdapterTest {
 
     @Page
     private SecurePortal securePortal;
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLServletsAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLServletsAdapterTest.java
index d6d52f2..3f9dabb 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLServletsAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLServletsAdapterTest.java
@@ -22,6 +22,7 @@ import org.jboss.arquillian.graphene.page.Page;
 import org.jboss.shrinkwrap.api.spec.WebArchive;
 import org.junit.Assert;
 import org.junit.Test;
+
 import org.keycloak.admin.client.resource.ClientResource;
 import org.keycloak.admin.client.resource.ProtocolMappersResource;
 import org.keycloak.admin.client.resource.RoleScopeResource;
@@ -71,6 +72,7 @@ import org.keycloak.testsuite.auth.page.login.SAMLIDPInitiatedLogin;
 import org.keycloak.testsuite.page.AbstractPage;
 import org.keycloak.testsuite.util.IOUtil;
 import org.keycloak.testsuite.util.UserBuilder;
+
 import org.openqa.selenium.By;
 import org.w3c.dom.Document;
 import org.xml.sax.SAXException;
@@ -104,6 +106,7 @@ import static org.junit.Assert.*;
 import static org.keycloak.representations.idm.CredentialRepresentation.PASSWORD;
 import static org.keycloak.testsuite.AbstractAuthTest.createUserRepresentation;
 import static org.keycloak.testsuite.admin.ApiUtil.createUserAndResetPasswordWithAdminClient;
+import static org.keycloak.testsuite.admin.Users.setPasswordFor;
 import static org.keycloak.testsuite.auth.page.AuthRealm.SAMLSERVLETDEMO;
 import static org.keycloak.testsuite.util.IOUtil.loadRealm;
 import static org.keycloak.testsuite.util.IOUtil.loadXML;
@@ -530,6 +533,14 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd
     }
 
     @Test
+    public void salesPostTestCompositeRoleForUser() {
+        UserRepresentation topGroupUser = createUserRepresentation("topGroupUser", "top@redhat.com", "", "", true);
+        setPasswordFor(topGroupUser, PASSWORD);
+
+        assertSuccessfulLogin(salesPostServletPage, topGroupUser, testRealmSAMLPostLoginPage, "principal=topgroupuser");
+    }
+
+    @Test
     public void salesPostTest() {
         testSuccessfulAndUnauthorizedLogin(salesPostServletPage, testRealmSAMLPostLoginPage);
     }
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 bce9117..990175d 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
@@ -19,6 +19,7 @@ package org.keycloak.testsuite.admin.authentication;
 
 import org.junit.Assert;
 import org.junit.Test;
+
 import org.keycloak.events.admin.OperationType;
 import org.keycloak.events.admin.ResourceType;
 import org.keycloak.representations.idm.AuthenticationExecutionExportRepresentation;
@@ -31,6 +32,10 @@ import javax.ws.rs.core.Response;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import javax.ws.rs.core.Response.Status;
+
+import static org.hamcrest.Matchers.*;
+import static org.keycloak.testsuite.util.Matchers.*;
 
 /**
  * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
@@ -195,7 +200,9 @@ public class FlowTest extends AbstractAuthenticationTest {
         // copy using existing alias as new name
         Response response = authMgmtResource.copy("browser", params);
         try {
-            Assert.assertEquals("Copy flow using the new alias of existing flow should fail", 409, response.getStatus());
+            Assert.assertThat("Copy flow using the new alias of existing flow should fail", response, statusCodeIs(Status.CONFLICT));
+            Assert.assertThat("Copy flow using the new alias of existing flow should fail", response, body(containsString("already exists")));
+            Assert.assertThat("Copy flow using the new alias of existing flow should fail", response, body(containsString("flow alias")));
         } finally {
             response.close();
         }
@@ -204,7 +211,7 @@ public class FlowTest extends AbstractAuthenticationTest {
         params.clear();
         response = authMgmtResource.copy("non-existent", params);
         try {
-            Assert.assertEquals("Copy non-existing flow", 404, response.getStatus());
+            Assert.assertThat("Copy non-existing flow", response, statusCodeIs(Status.NOT_FOUND));
         } finally {
             response.close();
         }
@@ -214,7 +221,7 @@ public class FlowTest extends AbstractAuthenticationTest {
         response = authMgmtResource.copy("browser", params);
         assertAdminEvents.assertEvent(REALM_NAME, OperationType.CREATE, AdminEventPaths.authCopyFlowPath("browser"), params, ResourceType.AUTH_FLOW);
         try {
-            Assert.assertEquals("Copy flow", 201, response.getStatus());
+            Assert.assertThat("Copy flow", response, statusCodeIs(Status.CREATED));
         } finally {
             response.close();
         }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/Matchers.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/Matchers.java
new file mode 100644
index 0000000..b66e728
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/Matchers.java
@@ -0,0 +1,77 @@
+/*
+ * 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;
+
+import org.keycloak.testsuite.util.matchers.ResponseBodyMatcher;
+import org.keycloak.testsuite.util.matchers.ResponseHeaderMatcher;
+import org.keycloak.testsuite.util.matchers.ResponseStatusCodeMatcher;
+
+import java.util.Map;
+import javax.ws.rs.core.Response;
+import org.hamcrest.Matcher;
+
+/**
+ * Additional hamcrest matchers for use in {@link org.junit.Assert#assertThat}.
+ * @author hmlnarik
+ */
+public class Matchers {
+
+    /**
+     * Matcher on HTTP status code of a {@link Response} instance.
+     * @param matcher
+     * @return
+     */
+    public static Matcher<Response> body(Matcher<String> matcher) {
+        return new ResponseBodyMatcher(matcher);
+    }
+
+    /**
+     * Matcher on HTTP status code of a {@link Response} instance.
+     * @param matcher
+     * @return
+     */
+    public static Matcher<Response> statusCode(Matcher<? extends Number> matcher) {
+        return new ResponseStatusCodeMatcher(matcher);
+    }
+
+    /**
+     * Matches when the HTTP status code of a {@link Response} instance is equal to the given code.
+     * @param expectedStatusCode
+     * @return
+     */
+    public static Matcher<Response> statusCodeIs(Response.Status expectedStatusCode) {
+        return new ResponseStatusCodeMatcher(org.hamcrest.Matchers.is(expectedStatusCode.getStatusCode()));
+    }
+
+    /**
+     * Matches when the HTTP status code of a {@link Response} instance is equal to the given code.
+     * @param expectedStatusCode
+     * @return
+     */
+    public static Matcher<Response> statusCodeIs(int expectedStatusCode) {
+        return new ResponseStatusCodeMatcher(org.hamcrest.Matchers.is(expectedStatusCode));
+    }
+
+    /**
+     * Matches when the HTTP status code of a {@link Response} instance is equal to the given code.
+     * @param expectedStatusCode
+     * @return
+     */
+    public static <T> Matcher<Response> header(Matcher<Map<String, T>> matcher) {
+        return new ResponseHeaderMatcher(matcher);
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/ResponseBodyMatcher.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/ResponseBodyMatcher.java
new file mode 100644
index 0000000..4a9788f
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/ResponseBodyMatcher.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.testsuite.util.matchers;
+
+import javax.ws.rs.core.Response;
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+
+/**
+ * Matcher for matching status code of {@link Response} instance.
+ * @author hmlnarik
+ */
+public class ResponseBodyMatcher extends BaseMatcher<Response> {
+
+    private final Matcher<String> matcher;
+
+    public ResponseBodyMatcher(Matcher<String> matcher) {
+        this.matcher = matcher;
+    }
+
+    @Override
+    public boolean matches(Object item) {
+        if (item instanceof Response) {
+            final Response rItem = (Response) item;
+            rItem.bufferEntity();
+            return this.matcher.matches(rItem.readEntity(String.class));
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public void describeTo(Description description) {
+        description.appendText("response body matches ").appendDescriptionOf(this.matcher);
+    }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/ResponseHeaderMatcher.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/ResponseHeaderMatcher.java
new file mode 100644
index 0000000..e2a3bb5
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/ResponseHeaderMatcher.java
@@ -0,0 +1,47 @@
+/*
+ * 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.matchers;
+
+import java.util.Map;
+import javax.ws.rs.core.Response;
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+
+/**
+ * Matcher for matching status code of {@link Response} instance.
+ * @author hmlnarik
+ */
+public class ResponseHeaderMatcher<T> extends BaseMatcher<Response> {
+
+    private final Matcher<Map<String, T>> matcher;
+
+    public ResponseHeaderMatcher(Matcher<Map<String, T>> matcher) {
+        this.matcher = matcher;
+    }
+
+    @Override
+    public boolean matches(Object item) {
+        return (item instanceof Response) && this.matcher.matches(((Response) item).getHeaders());
+    }
+
+    @Override
+    public void describeTo(Description description) {
+        description.appendText("response headers match ").appendDescriptionOf(this.matcher);
+    }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/ResponseStatusCodeMatcher.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/ResponseStatusCodeMatcher.java
new file mode 100644
index 0000000..a4e1885
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/ResponseStatusCodeMatcher.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 org.keycloak.testsuite.util.matchers;
+
+import javax.ws.rs.core.Response;
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+
+/**
+ * Matcher for matching status code of {@link Response} instance.
+ * @author hmlnarik
+ */
+public class ResponseStatusCodeMatcher extends BaseMatcher<Response> {
+
+    private final Matcher<? extends Number> matcher;
+
+    public ResponseStatusCodeMatcher(Matcher<? extends Number> matcher) {
+        this.matcher = matcher;
+    }
+
+    @Override
+    public boolean matches(Object item) {
+        return (item instanceof Response) && this.matcher.matches(((Response) item).getStatus());
+    }
+
+    @Override
+    public void describeTo(Description description) {
+        description.appendText("response status code matches ").appendDescriptionOf(this.matcher);
+    }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/demorealm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/demorealm.json
index b1f70e2..072ca40 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/demorealm.json
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/demorealm.json
@@ -49,6 +49,7 @@
                 { "type" : "password",
                     "value" : "password" }
             ],
+            "realmRoles": [ "realm-composite-role" ],
             "groups": [
                 "/top"
             ]
@@ -75,6 +76,14 @@
             {
                 "name": "admin",
                 "description": "Administrator privileges"
+            },
+            {
+                "name": "realm-composite-role",
+                "description": "Realm composite role containing user role",
+                "composite": true,
+                "composites": {
+                    "realm": ["user"]
+                }
             }
         ]
     },
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 8983c23..84e2520 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
@@ -868,6 +868,7 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates,
     $scope.samlForceNameIdFormat = false;
     $scope.disableAuthorizationTab = !client.authorizationServicesEnabled;
     $scope.disableServiceAccountRolesTab = !client.serviceAccountsEnabled;
+    $scope.disableCredentialsTab = client.publicClient;
 
     function updateProperties() {
         if (!$scope.client.attributes) {
diff --git a/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html
index 365d50a..f96801b 100755
--- a/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html
+++ b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html
@@ -8,7 +8,7 @@
 
     <ul class="nav nav-tabs"  data-ng-hide="create && !path[4]">
         <li ng-class="{active: !path[4]}"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}">{{:: 'settings' | translate}}</a></li>
-        <li ng-class="{active: path[4] == 'credentials'}" data-ng-show="!client.publicClient && client.protocol != 'saml'"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/credentials">{{:: 'credentials' | translate}}</a></li>
+        <li ng-class="{active: path[4] == 'credentials'}" data-ng-show="!disableCredentialsTab && !client.publicClient && client.protocol != 'saml'"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/credentials">{{:: 'credentials' | translate}}</a></li>
         <li ng-class="{active: path[4] == 'saml'}" data-ng-show="client.protocol == 'saml' && (client.attributes['saml.client.signature'] == 'true' || client.attributes['saml.encrypt'] == 'true')"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/saml/keys">{{:: 'saml-keys' | translate}}</a></li>
         <li ng-class="{active: path[4] == 'roles'}"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/roles">{{:: 'roles' | translate}}</a></li>
         <li ng-class="{active: path[4] == 'mappers'}" data-ng-show="!client.bearerOnly">
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 3c8f429..6bd6854 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
@@ -25,7 +25,9 @@
     <supplement name="default">
         <replacement placeholder="CACHE-CONTAINERS">
             <cache-container name="keycloak" jndi-name="infinispan/Keycloak">
-                <local-cache name="realms"/>
+                <local-cache name="realms">
+                    <eviction max-entries="10000" strategy="LRU"/>
+                </local-cache>
                 <local-cache name="users">
                     <eviction max-entries="10000" strategy="LRU"/>
                 </local-cache>
@@ -92,7 +94,9 @@
         <replacement placeholder="CACHE-CONTAINERS">
             <cache-container name="keycloak" jndi-name="infinispan/Keycloak">
                 <transport lock-timeout="60000"/>
-                <local-cache name="realms"/>
+                <local-cache name="realms">
+                    <eviction max-entries="10000" strategy="LRU"/>
+                </local-cache>
                 <local-cache name="users">
                     <eviction max-entries="10000" strategy="LRU"/>
                 </local-cache>