From 38a46726e4c6bf43507efa8e571b805128fbdf97 Mon Sep 17 00:00:00 2001 From: rmartinc Date: Thu, 23 Feb 2023 19:26:45 +0100 Subject: [PATCH] Implement UserInfoTokenMapper in HardcodedRole and RoleNameMapper mappers Closes https://github.com/keycloak/keycloak/issues/15624 --- .../protocol/oidc/mappers/HardcodedRole.java | 21 ++- .../protocol/oidc/mappers/RoleNameMapper.java | 26 ++- .../org/keycloak/testsuite/admin/ApiUtil.java | 9 ++ .../oauth/OIDCProtocolMappersTest.java | 153 ++++++++++++++++++ 4 files changed, 201 insertions(+), 8 deletions(-) diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/HardcodedRole.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/HardcodedRole.java index d2bbd073f3..10762d497d 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/HardcodedRole.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/HardcodedRole.java @@ -26,6 +26,7 @@ import org.keycloak.protocol.ProtocolMapperUtils; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.representations.AccessToken; +import org.keycloak.representations.IDToken; import org.keycloak.utils.RoleResolveUtil; import java.util.ArrayList; @@ -39,7 +40,7 @@ import java.util.Map; * @author Bill Burke * @version $Revision: 1 $ */ -public class HardcodedRole extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper { +public class HardcodedRole extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper, UserInfoTokenMapper { private static final List configProperties = new ArrayList<>(); @@ -87,9 +88,25 @@ public class HardcodedRole extends AbstractOIDCProtocolMapper implements OIDCAcc return ProtocolMapperUtils.PRIORITY_HARDCODED_ROLE_MAPPER; } + @Override + public AccessToken transformUserInfoToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, + UserSessionModel userSession, ClientSessionContext clientSessionCtx) { + // the mapper is always executed and then other role mappers decide if the claims are really set to the token + setClaim(token, mappingModel, userSession, session, clientSessionCtx); + return token; + } + @Override public AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionContext clientSessionCtx) { + // the mapper is always executed and then other role mappers decide if the claims are really set to the token + setClaim(token, mappingModel, userSession, session, clientSessionCtx); + return token; + } + + @Override + protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession, KeycloakSession session, + ClientSessionContext clientSessionCtx) { String role = mappingModel.getConfig().get(ROLE_CONFIG); String[] scopedRole = KeycloakModelUtils.parseRole(role); @@ -102,8 +119,6 @@ public class HardcodedRole extends AbstractOIDCProtocolMapper implements OIDCAcc AccessToken.Access access = RoleResolveUtil.getResolvedRealmRoles(session, clientSessionCtx, true); access.addRole(role); } - - return token; } public static ProtocolMapperModel create(String name, diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/RoleNameMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/RoleNameMapper.java index 3792c22171..4316d0ae32 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/RoleNameMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/RoleNameMapper.java @@ -26,6 +26,7 @@ import org.keycloak.protocol.ProtocolMapperUtils; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.representations.AccessToken; +import org.keycloak.representations.IDToken; import org.keycloak.utils.RoleResolveUtil; import java.util.ArrayList; @@ -39,7 +40,7 @@ import java.util.Map; * @author Bill Burke * @version $Revision: 1 $ */ -public class RoleNameMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper { +public class RoleNameMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper, UserInfoTokenMapper { private static final List configProperties = new ArrayList<>(); @@ -94,9 +95,25 @@ public class RoleNameMapper extends AbstractOIDCProtocolMapper implements OIDCAc return ProtocolMapperUtils.PRIORITY_ROLE_NAMES_MAPPER; } + @Override + public AccessToken transformUserInfoToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, + UserSessionModel userSession, ClientSessionContext clientSessionCtx) { + // the mapper is always executed and then other role mappers decide if the claims are really set to the token + setClaim(token, mappingModel, userSession, session, clientSessionCtx); + return token; + } + @Override public AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionContext clientSessionCtx) { + // the mapper is always executed and then other role mappers decide if the claims are really set to the token + setClaim(token, mappingModel, userSession, session, clientSessionCtx); + return token; + } + + @Override + protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession, KeycloakSession session, + ClientSessionContext clientSessionCtx) { String role = mappingModel.getConfig().get(ROLE_CONFIG); String newName = mappingModel.getConfig().get(NEW_ROLE_NAME); @@ -106,12 +123,12 @@ public class RoleNameMapper extends AbstractOIDCProtocolMapper implements OIDCAc String roleName = scopedRole[1]; if (appName != null) { AccessToken.Access access = RoleResolveUtil.getResolvedClientRoles(session, clientSessionCtx, appName, false); - if (access == null) return token; - if (!access.getRoles().contains(roleName)) return token; + if (access == null) return; + if (!access.getRoles().contains(roleName)) return; access.getRoles().remove(roleName); } else { AccessToken.Access access = RoleResolveUtil.getResolvedRealmRoles(session, clientSessionCtx, false); - if (access == null || !access.getRoles().contains(roleName)) return token; + if (access == null || !access.getRoles().contains(roleName)) return; access.getRoles().remove(roleName); } @@ -125,7 +142,6 @@ public class RoleNameMapper extends AbstractOIDCProtocolMapper implements OIDCAc } access.addRole(newRoleName); - return token; } public static ProtocolMapperModel create(String name, diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/admin/ApiUtil.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/admin/ApiUtil.java index 246d77dc11..5d31082280 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/admin/ApiUtil.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/admin/ApiUtil.java @@ -115,6 +115,15 @@ public class ApiUtil { return null; } + public static ProtocolMapperRepresentation findProtocolMapperByName(ClientScopeResource scope, String name) { + for (ProtocolMapperRepresentation p : scope.getProtocolMappers().getMappers()) { + if (p.getName().equals(name)) { + return p; + } + } + return null; + } + public static ClientScopeResource findClientScopeByName(RealmResource realm, String clientScopeName) { for (ClientScopeRepresentation clientScope : realm.clientScopes().findAll()) { if (clientScopeName.equals(clientScope.getName())) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OIDCProtocolMappersTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OIDCProtocolMappersTest.java index ca4a0f852e..c99f266abb 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OIDCProtocolMappersTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OIDCProtocolMappersTest.java @@ -73,10 +73,12 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.arrayContainingInAnyOrder; import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.hasItems; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.isEmptyOrNullString; +import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; @@ -1475,6 +1477,157 @@ public class OIDCProtocolMappersTest extends AbstractKeycloakTest { } } + private void checkRealmAccessInOtherClaims(Map otherClaims, String shouldExistRole, String shouldNotExistRole) { + assertThat(otherClaims.get("realm_access"), CoreMatchers.instanceOf(Map.class)); + Map access = (Map) otherClaims.get("realm_access"); + assertThat(access.get("roles"), CoreMatchers.instanceOf(Collection.class)); + Collection roles = (Collection) access.get("roles"); + if (shouldExistRole != null) { + assertThat(roles, hasItem(shouldExistRole)); + } + if (shouldNotExistRole != null) { + assertThat(roles, not(hasItem(shouldNotExistRole))); + } + } + + private void checkClientAccessInOtherClaims(Map otherClaims, String app, String shouldExistRole, String shouldNotExistRole) { + assertThat(otherClaims.get("resource_access"), CoreMatchers.instanceOf(Map.class)); + Map access = (Map) otherClaims.get("resource_access"); + assertThat(access.get(app), CoreMatchers.instanceOf(Map.class)); + access = (Map) access.get(app); + assertThat(access.get("roles"), CoreMatchers.instanceOf(Collection.class)); + Collection roles = (Collection) access.get("roles"); + if (shouldExistRole != null) { + assertThat(roles, hasItem(shouldExistRole)); + } + if (shouldNotExistRole != null) { + assertThat(roles, not(hasItem(shouldNotExistRole))); + } + } + + private Map modifyScopeRolesMapperToBeIncludedInAll(ClientScopeResource rolesScope, ProtocolMapperRepresentation mapper) { + Map config = new HashMap<>(mapper.getConfig()); + mapper.getConfig().put(OIDCAttributeMapperHelper.INCLUDE_IN_USERINFO, "true"); + mapper.getConfig().put(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, "true"); + mapper.getConfig().put(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN, "true"); + rolesScope.getProtocolMappers().update(mapper.getId(), mapper); + return config; + } + + @Test + public void testHardcodeRoleAll() throws Exception { + RealmResource testRealm = adminClient.realm("test"); + ClientResource app = findClientResourceByClientId(testRealm, "test-app"); + // create two hardcoded realm mappers for realm and client + String hardcodedRoleRealmMapperId, hardcodedRoleClientMapperId; + try (Response resp = app.getProtocolMappers().createMapper(createHardcodedRole("hardcoded-realm", "hardcoded"))) { + hardcodedRoleRealmMapperId = ApiUtil.getCreatedId(resp); + } + try (Response resp = app.getProtocolMappers().createMapper(createHardcodedRole("hardcoded-app", "test-app.hardcoded"))) { + hardcodedRoleClientMapperId = ApiUtil.getCreatedId(resp); + } + // modify the default role mappers to be included in access, ID and user-info + ClientScopeResource rolesScope = ApiUtil.findClientScopeByName(testRealm, OIDCLoginProtocolFactory.ROLES_SCOPE); + ProtocolMapperRepresentation realmRolesMapper = ApiUtil.findProtocolMapperByName(rolesScope, OIDCLoginProtocolFactory.REALM_ROLES); + Map configRealmRoles = modifyScopeRolesMapperToBeIncludedInAll(rolesScope, realmRolesMapper); + ProtocolMapperRepresentation clientRolesMapper = ApiUtil.findProtocolMapperByName(rolesScope, OIDCLoginProtocolFactory.CLIENT_ROLES); + Map configClientRoles = modifyScopeRolesMapperToBeIncludedInAll(rolesScope, clientRolesMapper); + + // check that the hardcoded mappers are in the three responses + try { + OAuthClient.AccessTokenResponse response = browserLogin("password", "test-user@localhost", "password"); + + // check hardcoded roles in access token + AccessToken accessToken = oauth.verifyToken(response.getAccessToken()); + assertThat(accessToken.getRealmAccess().getRoles(), hasItem("hardcoded")); + assertNotNull(accessToken.getResourceAccess("test-app")); + assertThat(accessToken.getResourceAccess("test-app").getRoles(), hasItem("hardcoded")); + + // in ID token + IDToken idToken = oauth.verifyIDToken(response.getIdToken()); + checkRealmAccessInOtherClaims(idToken.getOtherClaims(), "hardcoded", null); + checkClientAccessInOtherClaims(idToken.getOtherClaims(), "test-app", "hardcoded", null); + + // in the user info + Client client = AdminClientUtil.createResteasyClient(); + try { + Response userInfoResponse = UserInfoClientUtil.executeUserInfoRequest_getMethod(client, response.getAccessToken()); + UserInfo userInfo = userInfoResponse.readEntity(UserInfo.class); + assertEquals("test-user@localhost", userInfo.getPreferredUsername()); + checkRealmAccessInOtherClaims(userInfo.getOtherClaims(), "hardcoded", null); + checkClientAccessInOtherClaims(userInfo.getOtherClaims(), "test-app", "hardcoded", null); + } finally { + client.close(); + } + } finally { + // reset the roles client scopes + app.getProtocolMappers().delete(hardcodedRoleRealmMapperId); + app.getProtocolMappers().delete(hardcodedRoleClientMapperId); + realmRolesMapper.setConfig(configRealmRoles); + rolesScope.getProtocolMappers().update(realmRolesMapper.getId(), realmRolesMapper); + clientRolesMapper.setConfig(configClientRoles); + rolesScope.getProtocolMappers().update(clientRolesMapper.getId(), clientRolesMapper); + } + } + + @Test + public void testRoleNameMapperAll() throws Exception { + RealmResource testRealm = adminClient.realm("test"); + ClientResource app = findClientResourceByClientId(testRealm, "test-app"); + // create two role name mappers for realm and client + String realmRoleNameMapperId, clientRoleNameMapperId; + try (Response resp = app.getProtocolMappers().createMapper(createRoleNameMapper("rename-realm-role", "user", "realm-user"))) { + realmRoleNameMapperId = ApiUtil.getCreatedId(resp); + } + try (Response resp = app.getProtocolMappers().createMapper(createRoleNameMapper("rename-app-role", "test-app.customer-user", "test-app.test-app-user"))) { + clientRoleNameMapperId = ApiUtil.getCreatedId(resp); + } + // modify the default role mappers to be included in access, ID and user-info + ClientScopeResource rolesScope = ApiUtil.findClientScopeByName(testRealm, OIDCLoginProtocolFactory.ROLES_SCOPE); + ProtocolMapperRepresentation realmRolesMapper = ApiUtil.findProtocolMapperByName(rolesScope, OIDCLoginProtocolFactory.REALM_ROLES); + Map configRealmRoles = modifyScopeRolesMapperToBeIncludedInAll(rolesScope, realmRolesMapper); + ProtocolMapperRepresentation clientRolesMapper = ApiUtil.findProtocolMapperByName(rolesScope, OIDCLoginProtocolFactory.CLIENT_ROLES); + Map configClientRoles = modifyScopeRolesMapperToBeIncludedInAll(rolesScope, clientRolesMapper); + + // check that the role mappers are executed in the three responses + try { + OAuthClient.AccessTokenResponse response = browserLogin("password", "test-user@localhost", "password"); + + // check mapped roles are in access token and not the original ones + AccessToken accessToken = oauth.verifyToken(response.getAccessToken()); + assertThat(accessToken.getRealmAccess().getRoles(), hasItem("realm-user")); + assertThat(accessToken.getRealmAccess().getRoles(), not(hasItem("user"))); + assertNotNull(accessToken.getResourceAccess("test-app")); + assertThat(accessToken.getResourceAccess("test-app").getRoles(), hasItem("test-app-user")); + assertThat(accessToken.getResourceAccess("test-app").getRoles(), not(hasItem("customer-user"))); + + // same in ID token + IDToken idToken = oauth.verifyIDToken(response.getIdToken()); + checkRealmAccessInOtherClaims(idToken.getOtherClaims(), "realm-user", "user"); + checkClientAccessInOtherClaims(idToken.getOtherClaims(), "test-app", "test-app-user", "customer-user"); + + // same in user info + Client client = AdminClientUtil.createResteasyClient(); + try { + Response userInfoResponse = UserInfoClientUtil.executeUserInfoRequest_getMethod(client, response.getAccessToken()); + UserInfo userInfo = userInfoResponse.readEntity(UserInfo.class); + assertEquals("test-user@localhost", userInfo.getPreferredUsername()); + checkRealmAccessInOtherClaims(userInfo.getOtherClaims(), "realm-user", "user"); + checkClientAccessInOtherClaims(userInfo.getOtherClaims(), "test-app", "test-app-user", "customer-user"); + } finally { + client.close(); + } + } finally { + // reset the roles client scopes + app.getProtocolMappers().delete(realmRoleNameMapperId); + app.getProtocolMappers().delete(clientRoleNameMapperId); + realmRolesMapper.setConfig(configRealmRoles); + rolesScope.getProtocolMappers().update(realmRolesMapper.getId(), realmRolesMapper); + clientRolesMapper.setConfig(configClientRoles); + rolesScope.getProtocolMappers().update(clientRolesMapper.getId(), clientRolesMapper); + } + } + @Test @EnableFeature(value = Profile.Feature.DYNAMIC_SCOPES, skipRestart = true) public void executeTokenMappersOnDynamicScopes() {