KEYCLOAK-3081: Add client mapper to map user roles to token

Introduced two new client protocol mappers to propagate assigned user client / realm roles to a JWT ID/Access Token.
Each protocol mapper supports to use a prefix string that is prepended to each role name.

 The client role protocol mapper can specify from which client the roles should be considered.
 Composite Roles are resolved recursively.

Background:
Some OpenID Connect integrations like mod_auth_openidc don't support analyzing deeply nested or encoded structures.
In those scenarios it is helpful to be able to define custom client protocol mappers that allow to propagate a users's roles as a flat structure
(e.g. comma separated list) as a top-level  (ID/Access) Token attribute that can easily be matched with a regex.

In order to differentiate between client specific roles and realm roles it is possible to configure
both separately to be able to use the same role names with different contexts rendered as separate token attributes.
This commit is contained in:
Thomas Darimont 2016-06-03 15:31:16 +02:00
parent 31eee347d4
commit a2d1c8313d
6 changed files with 316 additions and 2 deletions

View file

@ -22,6 +22,7 @@ import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderFactory;
import java.lang.reflect.Method;
@ -31,6 +32,8 @@ import java.lang.reflect.Method;
* @version $Revision: 1 $
*/
public class ProtocolMapperUtils {
public static final String USER_ROLE = "user.role";
public static final String USER_ATTRIBUTE = "user.attribute";
public static final String USER_SESSION_NOTE = "user.session.note";
public static final String MULTIVALUED = "multivalued";
@ -38,6 +41,19 @@ public class ProtocolMapperUtils {
public static final String USER_MODEL_PROPERTY_HELP_TEXT = "usermodel.prop.tooltip";
public static final String USER_MODEL_ATTRIBUTE_LABEL = "usermodel.attr.label";
public static final String USER_MODEL_ATTRIBUTE_HELP_TEXT = "usermodel.attr.tooltip";
public static final String USER_MODEL_CLIENT_ROLE_MAPPING_CLIENT_ID = "usermodel.clientRoleMapping.clientId";
public static final String USER_MODEL_CLIENT_ROLE_MAPPING_CLIENT_ID_LABEL = "usermodel.clientRoleMapping.clientId.label";
public static final String USER_MODEL_CLIENT_ROLE_MAPPING_CLIENT_ID_HELP_TEXT = "usermodel.clientRoleMapping.clientId.tooltip";
public static final String USER_MODEL_CLIENT_ROLE_MAPPING_ROLE_PREFIX = "usermodel.clientRoleMapping.rolePrefix";
public static final String USER_MODEL_CLIENT_ROLE_MAPPING_ROLE_PREFIX_LABEL = "usermodel.clientRoleMapping.rolePrefix.label";
public static final String USER_MODEL_CLIENT_ROLE_MAPPING_ROLE_PREFIX_HELP_TEXT = "usermodel.clientRoleMapping.rolePrefix.tooltip";
public static final String USER_MODEL_REALM_ROLE_MAPPING_ROLE_PREFIX = "usermodel.realmRoleMapping.rolePrefix";
public static final String USER_MODEL_REALM_ROLE_MAPPING_ROLE_PREFIX_LABEL = "usermodel.realmRoleMapping.rolePrefix.label";
public static final String USER_MODEL_REALM_ROLE_MAPPING_ROLE_PREFIX_HELP_TEXT = "usermodel.realmRoleMapping.rolePrefix.tooltip";
public static final String USER_SESSION_MODEL_NOTE_LABEL = "userSession.modelNote.label";
public static final String USER_SESSION_MODEL_NOTE_HELP_TEXT = "userSession.modelNote.tooltip";
public static final String MULTIVALUED_LABEL = "multivalued.label";

View file

@ -0,0 +1,102 @@
/*
* 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.protocol.oidc.mappers;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.IDToken;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* Base class for mapping of user role mappings to an ID and Access Token claim.
*
* @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
*/
abstract class AbstractUserRoleMappingMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper, OIDCIDTokenMapper {
@Override
public AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session,
UserSessionModel userSession, ClientSessionModel clientSession) {
if (!OIDCAttributeMapperHelper.includeInAccessToken(mappingModel)) {
return token;
}
setClaim(token, mappingModel, userSession);
return token;
}
@Override
public IDToken transformIDToken(IDToken token, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) {
if (!OIDCAttributeMapperHelper.includeInIDToken(mappingModel)) {
return token;
}
setClaim(token, mappingModel, userSession);
return token;
}
protected abstract void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession);
/**
* Returns the role names extracted from the given {@code roleModels} while recursively traversing "Composite Roles".
* <p>
* Optionally prefixes each role name with the given {@code prefix}.
* </p>
*
* @param roleModels
* @param prefix the prefix to apply, may be {@literal null}
* @return
*/
protected Set<String> flattenRoleModelToRoleNames(Set<RoleModel> roleModels, String prefix) {
Set<String> roleNames = new LinkedHashSet<>();
Deque<RoleModel> stack = new ArrayDeque<>(roleModels);
while (!stack.isEmpty()) {
RoleModel current = stack.pop();
if (current.isComposite()) {
for (RoleModel compositeRoleModel : current.getComposites()) {
stack.push(compositeRoleModel);
}
}
String roleName = current.getName();
if (prefix != null && !prefix.trim().isEmpty()) {
roleName = prefix.trim() + roleName;
}
roleNames.add(roleName);
}
return roleNames;
}
}

View file

@ -0,0 +1,103 @@
/*
* 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.protocol.oidc.mappers;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.ProtocolMapperUtils;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.IDToken;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* Allows mapping of user client role mappings to an ID and Access Token claim.
*
* @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
*/
public class UserClientRoleMappingMapper extends AbstractUserRoleMappingMapper {
public static final String PROVIDER_ID = "oidc-usermodel-client-role-mapper";
private static final List<ProviderConfigProperty> CONFIG_PROPERTIES = new ArrayList<ProviderConfigProperty>();
static {
ProviderConfigProperty clientId = new ProviderConfigProperty();
clientId.setName(ProtocolMapperUtils.USER_MODEL_CLIENT_ROLE_MAPPING_CLIENT_ID);
clientId.setLabel(ProtocolMapperUtils.USER_MODEL_CLIENT_ROLE_MAPPING_CLIENT_ID_LABEL);
clientId.setHelpText(ProtocolMapperUtils.USER_MODEL_CLIENT_ROLE_MAPPING_CLIENT_ID_HELP_TEXT);
clientId.setType(ProviderConfigProperty.STRING_TYPE);
CONFIG_PROPERTIES.add(clientId);
ProviderConfigProperty clientRolePrefix = new ProviderConfigProperty();
clientRolePrefix.setName(ProtocolMapperUtils.USER_MODEL_CLIENT_ROLE_MAPPING_ROLE_PREFIX);
clientRolePrefix.setLabel(ProtocolMapperUtils.USER_MODEL_CLIENT_ROLE_MAPPING_ROLE_PREFIX_LABEL);
clientRolePrefix.setHelpText(ProtocolMapperUtils.USER_MODEL_CLIENT_ROLE_MAPPING_ROLE_PREFIX_HELP_TEXT);
clientRolePrefix.setType(ProviderConfigProperty.STRING_TYPE);
CONFIG_PROPERTIES.add(clientRolePrefix);
OIDCAttributeMapperHelper.addAttributeConfig(CONFIG_PROPERTIES);
}
public List<ProviderConfigProperty> getConfigProperties() {
return CONFIG_PROPERTIES;
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public String getDisplayType() {
return "User Client Role";
}
@Override
public String getDisplayCategory() {
return TOKEN_MAPPER_CATEGORY;
}
@Override
public String getHelpText() {
return "Map a user client role to a token claim.";
}
protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession) {
UserModel user = userSession.getUser();
String clientId = mappingModel.getConfig().get(ProtocolMapperUtils.USER_MODEL_CLIENT_ROLE_MAPPING_CLIENT_ID);
if (clientId != null) {
ClientModel clientModel = userSession.getRealm().getClientByClientId(clientId.trim());
Set<RoleModel> clientRoleMappings = user.getClientRoleMappings(clientModel);
String rolePrefix = mappingModel.getConfig().get(ProtocolMapperUtils.USER_MODEL_CLIENT_ROLE_MAPPING_ROLE_PREFIX);
Set<String> clientRoleNames = flattenRoleModelToRoleNames(clientRoleMappings, rolePrefix);
OIDCAttributeMapperHelper.mapClaim(token, mappingModel, clientRoleNames);
}
}
}

View file

@ -0,0 +1,87 @@
/*
* 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.protocol.oidc.mappers;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.ProtocolMapperUtils;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.IDToken;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* Allows mapping of user realm role mappings to an ID and Access Token claim.
*
* @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
*/
public class UserRealmRoleMappingMapper extends AbstractUserRoleMappingMapper {
public static final String PROVIDER_ID = "oidc-usermodel-realm-role-mapper";
private static final List<ProviderConfigProperty> CONFIG_PROPERTIES = new ArrayList<ProviderConfigProperty>();
static {
ProviderConfigProperty realmRolePrefix = new ProviderConfigProperty();
realmRolePrefix.setName(ProtocolMapperUtils.USER_MODEL_REALM_ROLE_MAPPING_ROLE_PREFIX);
realmRolePrefix.setLabel(ProtocolMapperUtils.USER_MODEL_REALM_ROLE_MAPPING_ROLE_PREFIX_LABEL);
realmRolePrefix.setHelpText(ProtocolMapperUtils.USER_MODEL_REALM_ROLE_MAPPING_ROLE_PREFIX_HELP_TEXT);
realmRolePrefix.setType(ProviderConfigProperty.STRING_TYPE);
CONFIG_PROPERTIES.add(realmRolePrefix);
OIDCAttributeMapperHelper.addAttributeConfig(CONFIG_PROPERTIES);
}
public List<ProviderConfigProperty> getConfigProperties() {
return CONFIG_PROPERTIES;
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public String getDisplayType() {
return "User Realm Role";
}
@Override
public String getDisplayCategory() {
return TOKEN_MAPPER_CATEGORY;
}
@Override
public String getHelpText() {
return "Map a user realm role to a token claim.";
}
protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession) {
UserModel user = userSession.getUser();
String rolePrefix = mappingModel.getConfig().get(ProtocolMapperUtils.USER_MODEL_REALM_ROLE_MAPPING_ROLE_PREFIX);
Set<String> realmRoleNames = flattenRoleModelToRoleNames(user.getRoleMappings(), rolePrefix);
OIDCAttributeMapperHelper.mapClaim(token, mappingModel, realmRoleNames);
}
}

View file

@ -32,6 +32,7 @@ org.keycloak.protocol.saml.mappers.UserAttributeStatementMapper
org.keycloak.protocol.saml.mappers.UserPropertyAttributeStatementMapper
org.keycloak.protocol.saml.mappers.UserSessionNoteStatementMapper
org.keycloak.protocol.saml.mappers.GroupMembershipMapper
org.keycloak.protocol.oidc.mappers.UserClientRoleMappingMapper
org.keycloak.protocol.oidc.mappers.UserRealmRoleMappingMapper

View file

@ -154,7 +154,12 @@ includeInIdToken.label=Add to ID token
includeInIdToken.tooltip=Should the claim be added to the ID token?
includeInAccessToken.label=Add to access token
includeInAccessToken.tooltip=Should the claim be added to the access token?
usermodel.clientRoleMapping.clientId.label=Client ID
usermodel.clientRoleMapping.clientId.tooltip=Client ID for role mappings
usermodel.clientRoleMapping.rolePrefix.label=Client Role prefix
usermodel.clientRoleMapping.rolePrefix.tooltip=A prefix for each client role (optional).
usermodel.realmRoleMapping.rolePrefix.label=Realm Role prefix
usermodel.realmRoleMapping.rolePrefix.tooltip=A prefix for each Realm Role (optional).
# client details
clients.tooltip=Clients are trusted browser apps and web services in a realm. These clients can request a login. You can also define client specific roles.