Implement UserInfoTokenMapper in HardcodedRole and RoleNameMapper mappers
Closes https://github.com/keycloak/keycloak/issues/15624
This commit is contained in:
parent
8abe984844
commit
38a46726e4
4 changed files with 201 additions and 8 deletions
|
@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class HardcodedRole extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper {
|
||||
public class HardcodedRole extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper, UserInfoTokenMapper {
|
||||
|
||||
private static final List<ProviderConfigProperty> 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,
|
||||
|
|
|
@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class RoleNameMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper {
|
||||
public class RoleNameMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper, UserInfoTokenMapper {
|
||||
|
||||
private static final List<ProviderConfigProperty> 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,
|
||||
|
|
|
@ -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())) {
|
||||
|
|
|
@ -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<String, Object> 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<String> roles = (Collection<String>) access.get("roles");
|
||||
if (shouldExistRole != null) {
|
||||
assertThat(roles, hasItem(shouldExistRole));
|
||||
}
|
||||
if (shouldNotExistRole != null) {
|
||||
assertThat(roles, not(hasItem(shouldNotExistRole)));
|
||||
}
|
||||
}
|
||||
|
||||
private void checkClientAccessInOtherClaims(Map<String, Object> 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<String> roles = (Collection<String>) access.get("roles");
|
||||
if (shouldExistRole != null) {
|
||||
assertThat(roles, hasItem(shouldExistRole));
|
||||
}
|
||||
if (shouldNotExistRole != null) {
|
||||
assertThat(roles, not(hasItem(shouldNotExistRole)));
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, String> modifyScopeRolesMapperToBeIncludedInAll(ClientScopeResource rolesScope, ProtocolMapperRepresentation mapper) {
|
||||
Map<String, String> 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<String, String> configRealmRoles = modifyScopeRolesMapperToBeIncludedInAll(rolesScope, realmRolesMapper);
|
||||
ProtocolMapperRepresentation clientRolesMapper = ApiUtil.findProtocolMapperByName(rolesScope, OIDCLoginProtocolFactory.CLIENT_ROLES);
|
||||
Map<String, String> 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<String, String> configRealmRoles = modifyScopeRolesMapperToBeIncludedInAll(rolesScope, realmRolesMapper);
|
||||
ProtocolMapperRepresentation clientRolesMapper = ApiUtil.findProtocolMapperByName(rolesScope, OIDCLoginProtocolFactory.CLIENT_ROLES);
|
||||
Map<String, String> 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() {
|
||||
|
|
Loading…
Reference in a new issue