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:
parent
31eee347d4
commit
a2d1c8313d
6 changed files with 316 additions and 2 deletions
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in a new issue