keycloak-aplcache

KEYCLOAK-8175 Possibility of clientScope not being used if

10/24/2018 7:48:16 AM

Details

diff --git a/server-spi/src/main/java/org/keycloak/models/ClientSessionContext.java b/server-spi/src/main/java/org/keycloak/models/ClientSessionContext.java
index 3edfeb6..3320797 100644
--- a/server-spi/src/main/java/org/keycloak/models/ClientSessionContext.java
+++ b/server-spi/src/main/java/org/keycloak/models/ClientSessionContext.java
@@ -32,6 +32,9 @@ public interface ClientSessionContext {
 
     Set<ClientScopeModel> getClientScopes();
 
+    /**
+     * @return expanded roles (composite roles already applied)
+     */
     Set<RoleModel> getRoles();
 
     Set<ProtocolMapperModel> getProtocolMappers();
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 1dd7267..025937e 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
@@ -19,11 +19,13 @@ package org.keycloak.models.utils;
 
 import org.keycloak.models.GroupModel;
 import org.keycloak.models.RoleModel;
+import org.keycloak.models.UserModel;
 
 import java.util.ArrayDeque;
 import java.util.Deque;
 import java.util.HashSet;
 import java.util.Set;
+import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
 
@@ -105,30 +107,67 @@ public class RoleUtils {
     /**
      * Recursively expands composite roles into their composite.
      * @param role
+     * @param visited Track roles, which were already visited. Those will be ignored and won't be added to the stream. Besides that,
+     *                the "visited" set itself will be updated as a result of this method call and all the tracked roles will be added to it
      * @return Stream of containing all of the composite roles and their components.
      */
-    public static Stream<RoleModel> expandCompositeRolesStream(RoleModel role) {
+    private static Stream<RoleModel> expandCompositeRolesStream(RoleModel role, Set<RoleModel> visited) {
         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);
-                  });
+
+        if (!visited.contains(role)) {
+            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 -> !visited.contains(r))
+                            .forEach(r -> {
+                                visited.add(r);
+                                stack.add(r);
+                            });
+                }
             }
         }
 
         return sb.build();
     }
 
+
+    /**
+     * @param roles
+     * @return new set with composite roles expanded
+     */
+    public static Set<RoleModel> expandCompositeRoles(Set<RoleModel> roles) {
+        Set<RoleModel> visited = new HashSet<>();
+
+        return roles.stream()
+                .flatMap(roleModel -> RoleUtils.expandCompositeRolesStream(roleModel, visited))
+                .collect(Collectors.toSet());
+    }
+
+
+    /**
+     * @param user
+     * @return all user role mappings including all groups of user. Composite roles will be expanded
+     */
+    public static Set<RoleModel> getDeepUserRoleMappings(UserModel user) {
+        Set<RoleModel> roleMappings = new HashSet<>(user.getRoleMappings());
+        for (GroupModel group : user.getGroups()) {
+            addGroupRoles(group, roleMappings);
+        }
+
+        return expandCompositeRoles(roleMappings);
+    }
+
+
+    private static void addGroupRoles(GroupModel group, Set<RoleModel> roleMappings) {
+        roleMappings.addAll(group.getRoleMappings());
+        if (group.getParentId() == null) return;
+        addGroupRoles(group.getParent(), roleMappings);
+    }
+
 }
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
index ba36588..a007c71 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
@@ -35,9 +35,7 @@ import org.keycloak.models.AuthenticatedClientSessionModel;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.ClientScopeModel;
 import org.keycloak.models.ClientSessionContext;
-import org.keycloak.models.GroupModel;
 import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.KeycloakSessionFactory;
 import org.keycloak.models.ProtocolMapperModel;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.RoleModel;
@@ -46,13 +44,13 @@ import org.keycloak.models.UserModel;
 import org.keycloak.models.UserSessionModel;
 import org.keycloak.models.UserSessionProvider;
 import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.models.utils.RoleUtils;
 import org.keycloak.protocol.ProtocolMapper;
 import org.keycloak.protocol.ProtocolMapperUtils;
 import org.keycloak.protocol.oidc.mappers.OIDCAccessTokenMapper;
 import org.keycloak.protocol.oidc.mappers.OIDCIDTokenMapper;
 import org.keycloak.protocol.oidc.mappers.UserInfoTokenMapper;
 import org.keycloak.protocol.oidc.utils.OIDCResponseType;
-import org.keycloak.protocol.oidc.utils.WebOriginsUtils;
 import org.keycloak.representations.AccessToken;
 import org.keycloak.representations.AccessTokenResponse;
 import org.keycloak.representations.IDToken;
@@ -86,20 +84,6 @@ public class TokenManager {
     private static final Logger logger = Logger.getLogger(TokenManager.class);
     private static final String JWT = "JWT";
 
-    public static void applyScope(RoleModel role, RoleModel scope, Set<RoleModel> visited, Set<RoleModel> requested) {
-        if (visited.contains(scope)) return;
-        visited.add(scope);
-        if (role.hasRole(scope)) {
-            requested.add(scope);
-            return;
-        }
-        if (!scope.isComposite()) return;
-
-        for (RoleModel contained : scope.getComposites()) {
-            applyScope(role, contained, visited, requested);
-        }
-    }
-
     public static class TokenValidation {
         public final UserModel user;
         public final UserSessionModel userSession;
@@ -468,28 +452,14 @@ public class TokenManager {
     }
 
 
-    private static void addGroupRoles(GroupModel group, Set<RoleModel> roleMappings) {
-        roleMappings.addAll(group.getRoleMappings());
-        if (group.getParentId() == null) return;
-        addGroupRoles(group.getParent(), roleMappings);
-    }
-
-
     public static Set<RoleModel> getAccess(UserModel user, ClientModel client, Set<ClientScopeModel> clientScopes) {
-        Set<RoleModel> requestedRoles = new HashSet<RoleModel>();
-
-        Set<RoleModel> mappings = user.getRoleMappings();
-        Set<RoleModel> roleMappings = new HashSet<>();
-        roleMappings.addAll(mappings);
-        for (GroupModel group : user.getGroups()) {
-            addGroupRoles(group, roleMappings);
-        }
+        Set<RoleModel> roleMappings = RoleUtils.getDeepUserRoleMappings(user);
 
         if (client.isFullScopeAllowed()) {
             if (logger.isTraceEnabled()) {
                 logger.tracef("Using full scope for client %s", client.getClientId());
             }
-            requestedRoles = roleMappings;
+            return roleMappings;
         } else {
             Set<RoleModel> scopeMappings = new HashSet<>();
 
@@ -504,15 +474,14 @@ public class TokenManager {
                 scopeMappings.addAll(clientScope.getScopeMappings());
             }
 
-            for (RoleModel role : roleMappings) {
-                for (RoleModel desiredRole : scopeMappings) {
-                    Set<RoleModel> visited = new HashSet<RoleModel>();
-                    applyScope(role, desiredRole, visited, requestedRoles);
-                }
-            }
-        }
+            // 3 - Expand scope mappings
+            scopeMappings = RoleUtils.expandCompositeRoles(scopeMappings);
 
-        return requestedRoles;
+            // Intersection of expanded user roles and expanded scopeMappings
+            roleMappings.retainAll(scopeMappings);
+
+            return roleMappings;
+        }
     }
 
 
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 dd70b53..dd2587d 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,6 +23,7 @@ import org.keycloak.models.ClientSessionContext;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
 import org.keycloak.models.ProtocolMapperModel;
+import org.keycloak.models.RoleModel;
 import org.keycloak.models.UserSessionModel;
 import org.keycloak.models.utils.RoleUtils;
 import org.keycloak.protocol.ProtocolMapper;
@@ -32,6 +33,7 @@ import org.keycloak.provider.ProviderConfigProperty;
 
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
@@ -144,7 +146,6 @@ public class RoleListMapper extends AbstractSAMLProtocolMapper implements SAMLRo
 
         List<String> allRoleNames = clientSessionCtx.getRoles().stream()
           // todo need a role mapping
-          .flatMap(RoleUtils::expandCompositeRolesStream)
           .map(roleModel -> roleNameMappers.stream()
             .map(entry -> entry.mapper.mapName(entry.model, roleModel))
             .filter(Objects::nonNull)
diff --git a/services/src/main/java/org/keycloak/services/managers/UserSessionManager.java b/services/src/main/java/org/keycloak/services/managers/UserSessionManager.java
index 1283f5f..44dcb3a 100644
--- a/services/src/main/java/org/keycloak/services/managers/UserSessionManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/UserSessionManager.java
@@ -131,13 +131,7 @@ public class UserSessionManager {
         }
 
         // Check if offline_access is allowed here. Even through composite roles
-        for (RoleModel role : clientSessionCtx.getRoles()) {
-            if (role.hasRole(offlineAccessRole)) {
-                return true;
-            }
-        }
-
-        return false;
+        return clientSessionCtx.getRoles().contains(offlineAccessRole);
     }
 
     private UserSessionModel createOfflineUserSession(UserModel user, UserSessionModel userSession) {
diff --git a/services/src/main/java/org/keycloak/services/util/DefaultClientSessionContext.java b/services/src/main/java/org/keycloak/services/util/DefaultClientSessionContext.java
index debd700..2c127ed 100644
--- a/services/src/main/java/org/keycloak/services/util/DefaultClientSessionContext.java
+++ b/services/src/main/java/org/keycloak/services/util/DefaultClientSessionContext.java
@@ -30,7 +30,7 @@ import org.keycloak.models.ProtocolMapperModel;
 import org.keycloak.models.RoleModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.utils.KeycloakModelUtils;
-import org.keycloak.models.utils.RepresentationToModel;
+import org.keycloak.models.utils.RoleUtils;
 import org.keycloak.protocol.oidc.OIDCLoginProtocol;
 import org.keycloak.protocol.oidc.TokenManager;
 import org.keycloak.util.TokenUtil;
@@ -48,9 +48,14 @@ public class DefaultClientSessionContext implements ClientSessionContext {
     private final Set<String> clientScopeIds;
 
     private Set<ClientScopeModel> clientScopes;
+
+    //
     private Set<RoleModel> roles;
     private Set<ProtocolMapperModel> protocolMappers;
 
+    // All roles of user expanded. It doesn't yet take into account permitted clientScopes
+    private Set<RoleModel> userRoles;
+
     private DefaultClientSessionContext(AuthenticatedClientSessionModel clientSession, Set<String> clientScopeIds) {
         this.clientSession = clientSession;
         this.clientScopeIds = clientScopeIds;
@@ -82,9 +87,7 @@ public class DefaultClientSessionContext implements ClientSessionContext {
             clientScopeIds.add(clientScope.getId());
         }
 
-        DefaultClientSessionContext ctx = new DefaultClientSessionContext(clientSession, clientScopeIds);
-        ctx.clientScopes = new HashSet<>(clientScopes);
-        return ctx;
+        return new DefaultClientSessionContext(clientSession, clientScopeIds);
     }
 
 
@@ -122,7 +125,7 @@ public class DefaultClientSessionContext implements ClientSessionContext {
 
     @Override
     public Set<ProtocolMapperModel> getProtocolMappers() {
-        // Load roles if not yet present
+        // Load protocolMappers if not yet present
         if (protocolMappers == null) {
             protocolMappers = loadProtocolMappers();
         }
@@ -130,6 +133,15 @@ public class DefaultClientSessionContext implements ClientSessionContext {
     }
 
 
+    private Set<RoleModel> getUserRoles() {
+        // Load userRoles if not yet present
+        if (userRoles == null) {
+            userRoles = loadUserRoles();
+        }
+        return userRoles;
+    }
+
+
     @Override
     public String getScopeString() {
         StringBuilder builder = new StringBuilder();
@@ -172,13 +184,42 @@ public class DefaultClientSessionContext implements ClientSessionContext {
         for (String scopeId : clientScopeIds) {
             ClientScopeModel clientScope = KeycloakModelUtils.findClientScopeById(clientSession.getClient().getRealm(), scopeId);
             if (clientScope != null) {
-                clientScopes.add(clientScope);
+                if (isClientScopePermittedForUser(clientScope)) {
+                    clientScopes.add(clientScope);
+                } else {
+                    if (logger.isTraceEnabled()) {
+                        logger.tracef("User '%s' not permitted to have client scope '%s'",
+                                clientSession.getUserSession().getUser().getUsername(), clientScope.getName());
+                    }
+                }
             }
         }
         return clientScopes;
     }
 
 
+    // Return true if clientScope can be used by the user.
+    private boolean isClientScopePermittedForUser(ClientScopeModel clientScope) {
+        if (clientScope instanceof ClientModel) {
+            return true;
+        }
+
+        Set<RoleModel> clientScopeRoles = clientScope.getScopeMappings();
+
+        // Client scope is automatically permitted if it doesn't have any role scope mappings
+        if (clientScopeRoles.isEmpty()) {
+            return true;
+        }
+
+        // Expand (resolve composite roles)
+        clientScopeRoles = RoleUtils.expandCompositeRoles(clientScopeRoles);
+
+        // Check if expanded roles of clientScope has any intersection with expanded roles of user. If not, it is not permitted
+        clientScopeRoles.retainAll(getUserRoles());
+        return !clientScopeRoles.isEmpty();
+    }
+
+
     private Set<RoleModel> loadRoles() {
         UserModel user = clientSession.getUserSession().getUser();
         ClientModel client = clientSession.getClient();
@@ -212,4 +253,10 @@ public class DefaultClientSessionContext implements ClientSessionContext {
         return protocolMappers;
     }
 
+
+    private Set<RoleModel> loadUserRoles() {
+        UserModel user = clientSession.getUserSession().getUser();
+        return RoleUtils.getDeepUserRoleMappings(user);
+    }
+
 }
diff --git a/services/src/main/java/org/keycloak/utils/RoleResolveUtil.java b/services/src/main/java/org/keycloak/utils/RoleResolveUtil.java
index 5f095ec..a674bd3 100644
--- a/services/src/main/java/org/keycloak/utils/RoleResolveUtil.java
+++ b/services/src/main/java/org/keycloak/utils/RoleResolveUtil.java
@@ -26,7 +26,6 @@ import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.RoleModel;
 import org.keycloak.representations.AccessToken;
-import org.keycloak.services.util.DefaultClientSessionContext;
 
 /**
  * Helper class to ensure that all the user's permitted roles (including composite roles) are loaded just once per request.
@@ -113,13 +112,13 @@ public class RoleResolveUtil {
         Set<RoleModel> requestedRoles = clientSessionCtx.getRoles();
         AccessToken token = new AccessToken();
         for (RoleModel role : requestedRoles) {
-            addComposites(token, role);
+            addToToken(token, role);
         }
         return token;
     }
 
 
-    private static void addComposites(AccessToken token, RoleModel role) {
+    private static void addToToken(AccessToken token, RoleModel role) {
         AccessToken.Access access = null;
         if (role.getContainer() instanceof RealmModel) {
             access = token.getRealmAccess();
@@ -139,12 +138,6 @@ public class RoleResolveUtil {
 
         }
         access.addRole(role.getName());
-        if (!role.isComposite()) return;
-
-        for (RoleModel composite : role.getComposites()) {
-            addComposites(token, composite);
-        }
-
     }
 
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountFormServiceTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountFormServiceTest.java
index 9267cbb..31c1055 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountFormServiceTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountFormServiceTest.java
@@ -1004,7 +1004,7 @@ public class AccountFormServiceTest extends AbstractTestRealmKeycloakTest {
         Assert.assertEquals("http://localhost:8180/auth/realms/test/account", accountEntry.getHref());
 
         AccountApplicationsPage.AppEntry testAppEntry = apps.get("test-app");
-        Assert.assertEquals(5, testAppEntry.getRolesAvailable().size());
+        Assert.assertEquals(6, testAppEntry.getRolesAvailable().size());
         Assert.assertTrue(testAppEntry.getRolesAvailable().contains("Offline access"));
         Assert.assertTrue(testAppEntry.getClientScopesGranted().contains("Full Access"));
         Assert.assertEquals("http://localhost:8180/auth/realms/master/app/auth", testAppEntry.getHref());
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCScopeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCScopeTest.java
index 944f304..005f8e0 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCScopeTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCScopeTest.java
@@ -18,6 +18,7 @@
 package org.keycloak.testsuite.oidc;
 
 import java.util.Arrays;
+import java.util.Collections;
 
 import javax.ws.rs.NotFoundException;
 import javax.ws.rs.core.Response;
@@ -44,6 +45,7 @@ import org.keycloak.representations.RefreshToken;
 import org.keycloak.representations.idm.ClientRepresentation;
 import org.keycloak.representations.idm.ClientScopeRepresentation;
 import org.keycloak.representations.idm.EventRepresentation;
+import org.keycloak.representations.idm.GroupRepresentation;
 import org.keycloak.representations.idm.RealmRepresentation;
 import org.keycloak.representations.idm.RoleRepresentation;
 import org.keycloak.representations.idm.UserRepresentation;
@@ -54,6 +56,7 @@ import org.keycloak.testsuite.pages.OAuthGrantPage;
 import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
 import org.keycloak.testsuite.util.ClientManager;
 import org.keycloak.testsuite.util.OAuthClient;
+import org.keycloak.testsuite.util.RoleBuilder;
 import org.keycloak.testsuite.util.UserBuilder;
 
 import static org.junit.Assert.assertEquals;
@@ -104,6 +107,51 @@ public class OIDCScopeTest extends AbstractOIDCScopeTest {
         RoleRepresentation role2 = new RoleRepresentation();
         role2.setName("role-2");
         testRealm.getRoles().getRealm().add(role2);
+
+        RoleRepresentation roleParent = RoleBuilder.create()
+                .name("role-parent")
+                .realmComposite("role-1")
+                .build();
+        testRealm.getRoles().getRealm().add(roleParent);
+
+        // Add sample group
+        GroupRepresentation group = new GroupRepresentation();
+        group.setName("group-role-1");
+        group.setRealmRoles(Collections.singletonList("role-1"));
+        testRealm.getGroups().add(group);
+
+        // Add more sample users
+        user = UserBuilder.create()
+                .username("role-1-user")
+                .enabled(true)
+                .password("password")
+                .addRoles("role-1")
+                .build();
+        testRealm.getUsers().add(user);
+
+        user = UserBuilder.create()
+                .username("role-2-user")
+                .enabled(true)
+                .password("password")
+                .addRoles("role-2")
+                .build();
+        testRealm.getUsers().add(user);
+
+        user = UserBuilder.create()
+                .username("role-parent-user")
+                .enabled(true)
+                .password("password")
+                .addRoles("role-parent")
+                .build();
+        testRealm.getUsers().add(user);
+
+        user = UserBuilder.create()
+                .username("group-role-1-user")
+                .enabled(true)
+                .password("password")
+                .addGroups("group-role-1")
+                .build();
+        testRealm.getUsers().add(user);
     }
 
     @Before
@@ -534,4 +582,78 @@ public class OIDCScopeTest extends AbstractOIDCScopeTest {
         testApp.removeOptionalClientScope(scope2Id);
     }
 
+
+    // Test that clientScope is NOT applied in case that user is not member of any role scoped to the clientScope (including composite roles)
+    @Test
+    public void testClientScopesPermissions() {
+        // Add 2 client scopes. Each with scope to 1 realm role
+        ClientScopeRepresentation clientScope1 = new ClientScopeRepresentation();
+        clientScope1.setName("scope-role-1");
+        clientScope1.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+        Response response = testRealm().clientScopes().create(clientScope1);
+        String scope1Id = ApiUtil.getCreatedId(response);
+        getCleanup().addClientScopeId(scope1Id);
+        response.close();
+
+        ClientScopeRepresentation clientScopeParent = new ClientScopeRepresentation();
+        clientScopeParent.setName("scope-role-parent");
+        clientScopeParent.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+        response = testRealm().clientScopes().create(clientScopeParent);
+        String scopeParentId = ApiUtil.getCreatedId(response);
+        getCleanup().addClientScopeId(scopeParentId);
+        response.close();
+
+        RoleRepresentation role1 = testRealm().roles().get("role-1").toRepresentation();
+        testRealm().clientScopes().get(scope1Id).getScopeMappings().realmLevel().add(Arrays.asList(role1));
+
+        RoleRepresentation roleParent = testRealm().roles().get("role-parent").toRepresentation();
+        testRealm().clientScopes().get(scopeParentId).getScopeMappings().realmLevel().add(Arrays.asList(roleParent));
+
+        // Add client scopes to our client
+        ClientResource testApp = ApiUtil.findClientByClientId(testRealm(), "test-app");
+        ClientRepresentation testAppRep = testApp.toRepresentation();
+        testApp.update(testAppRep);
+        testApp.addDefaultClientScope(scope1Id);
+        testApp.addDefaultClientScope(scopeParentId);
+
+        // role-1-user will have clientScope "scope-role-1" and also "scope-role-parent" due the composite role
+        testLoginAndClientScopesPermissions("role-1-user", "scope-role-1 scope-role-parent", "role-1");
+
+        // role-2-user won't have any of the "scope-role-1" or "scope-role-parent" applied as he is not member of "role-1" nor "role-parent"
+        testLoginAndClientScopesPermissions("role-2-user", "", "role-2");
+
+        // role-parent-user will have clientScope "scope-role-1" (due the composite role) and also "scope-role-parent"
+        testLoginAndClientScopesPermissions("role-parent-user", "scope-role-1 scope-role-parent", "role-1", "role-parent");
+
+        // group-role-1-user will have clientScope "scope-role-1" and also "scope-role-parent" due the composite role and due the fact that he is member of group
+        testLoginAndClientScopesPermissions("group-role-1-user", "scope-role-1 scope-role-parent", "role-1");
+
+
+        // Revert
+        testApp.removeOptionalClientScope(scope1Id);
+        testApp.removeOptionalClientScope(scopeParentId);
+    }
+
+
+    private void testLoginAndClientScopesPermissions(String username, String expectedRoleScopes, String... expectedRoles) {
+        String userId = ApiUtil.findUserByUsername(testRealm(), username).getId();
+
+        oauth.openLoginForm();
+        oauth.doLogin(username, "password");
+        EventRepresentation loginEvent = events.expectLogin()
+                .user(userId)
+                .assertEvent();
+
+        Tokens tokens = sendTokenRequest(loginEvent, userId,"openid email profile " + expectedRoleScopes, "test-app");
+        Assert.assertNames(tokens.accessToken.getRealmAccess().getRoles(), expectedRoles);
+
+        oauth.doLogout(tokens.refreshToken, "password");
+        events.expectLogout(tokens.idToken.getSessionState())
+                .client("test-app")
+                .user(userId)
+                .removeDetail(Details.REDIRECT_URI).assertEvent();
+    }
+
+
+
 }