diff --git a/core/src/main/java/org/keycloak/representations/idm/ProtocolMapperTypeRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/ProtocolMapperTypeRepresentation.java index 22192f664f2..205fb2bf9e6 100755 --- a/core/src/main/java/org/keycloak/representations/idm/ProtocolMapperTypeRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/ProtocolMapperTypeRepresentation.java @@ -28,6 +28,7 @@ public class ProtocolMapperTypeRepresentation { protected String name; protected String category; protected String helpText; + protected int priority; protected List properties; @@ -63,6 +64,14 @@ public class ProtocolMapperTypeRepresentation { this.helpText = helpText; } + public int getPriority() { + return priority; + } + + public void setPriority(int priority) { + this.priority = priority; + } + public List getProperties() { return properties; } diff --git a/server-spi-private/src/main/java/org/keycloak/migration/MigrationModelManager.java b/server-spi-private/src/main/java/org/keycloak/migration/MigrationModelManager.java index 1ffc8fe1f36..10ddab4aec2 100755 --- a/server-spi-private/src/main/java/org/keycloak/migration/MigrationModelManager.java +++ b/server-spi-private/src/main/java/org/keycloak/migration/MigrationModelManager.java @@ -40,6 +40,7 @@ import org.keycloak.migration.migrators.MigrateTo3_4_1; import org.keycloak.migration.migrators.MigrateTo3_4_2; import org.keycloak.migration.migrators.MigrateTo4_0_0; import org.keycloak.migration.migrators.MigrateTo4_2_0; +import org.keycloak.migration.migrators.MigrateTo4_6_0; import org.keycloak.migration.migrators.Migration; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; @@ -74,7 +75,8 @@ public class MigrationModelManager { new MigrateTo3_4_1(), new MigrateTo3_4_2(), new MigrateTo4_0_0(), - new MigrateTo4_2_0() + new MigrateTo4_2_0(), + new MigrateTo4_6_0() }; public static void migrate(KeycloakSession session) { diff --git a/server-spi-private/src/main/java/org/keycloak/migration/MigrationProvider.java b/server-spi-private/src/main/java/org/keycloak/migration/MigrationProvider.java index 0fcbd639065..3cb7e797ad3 100755 --- a/server-spi-private/src/main/java/org/keycloak/migration/MigrationProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/migration/MigrationProvider.java @@ -17,6 +17,7 @@ package org.keycloak.migration; +import org.keycloak.models.ClientScopeModel; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RealmModel; import org.keycloak.provider.Provider; @@ -42,4 +43,22 @@ public interface MigrationProvider extends Provider { void setupAdminCli(RealmModel realm); + + /** + * Add 'roles' client scope or return it if already exists + * + * @param realm + * @return created or already existing client scope 'roles' + */ + ClientScopeModel addOIDCRolesClientScope(RealmModel realm); + + + /** + * Add 'web-origins' client scope or return it if already exists + * + * @param realm + * @return created or already existing client scope 'web-origins' + */ + ClientScopeModel addOIDCWebOriginsClientScope(RealmModel realm); + } diff --git a/server-spi-private/src/main/java/org/keycloak/migration/migrators/MigrateTo4_6_0.java b/server-spi-private/src/main/java/org/keycloak/migration/migrators/MigrateTo4_6_0.java new file mode 100644 index 00000000000..717c2ed625e --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/migration/migrators/MigrateTo4_6_0.java @@ -0,0 +1,75 @@ +/* + * Copyright 2017 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.migration.migrators; + +import org.jboss.logging.Logger; +import org.keycloak.migration.MigrationProvider; +import org.keycloak.migration.ModelVersion; +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientScopeModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.representations.idm.RealmRepresentation; + + +/** + * @author Marek Posolda + */ +public class MigrateTo4_6_0 implements Migration { + + public static final ModelVersion VERSION = new ModelVersion("4.6.0"); + + private static final Logger LOG = Logger.getLogger(MigrateTo4_6_0.class); + + @Override + public ModelVersion getVersion() { + return VERSION; + } + + @Override + public void migrate(KeycloakSession session) { + session.realms().getRealms().stream().forEach(r -> { + migrateRealm(session, r, false); + }); + } + + @Override + public void migrateImport(KeycloakSession session, RealmModel realm, RealmRepresentation rep, boolean skipUserDependent) { + migrateRealm(session, realm, true); + } + + protected void migrateRealm(KeycloakSession session, RealmModel realm, boolean json) { + MigrationProvider migrationProvider = session.getProvider(MigrationProvider.class); + + // Create "roles" and "web-origins" clientScopes + ClientScopeModel rolesScope = migrationProvider.addOIDCRolesClientScope(realm); + ClientScopeModel webOriginsScope = migrationProvider.addOIDCWebOriginsClientScope(realm); + + LOG.debugf("Added '%s' and '%s' default client scopes", rolesScope.getName(), webOriginsScope.getName()); + + // Assign "roles" and "web-origins" clientScopes to all the OIDC clients + for (ClientModel client : realm.getClients()) { + if ((client.getProtocol()==null || "openid-connect".equals(client.getProtocol())) && (!client.isBearerOnly())) { + client.addClientScope(rolesScope, true); + client.addClientScope(webOriginsScope, true); + } + } + + LOG.debugf("Client scope '%s' assigned to all the clients", rolesScope.getName()); + } +} diff --git a/server-spi-private/src/main/java/org/keycloak/protocol/ProtocolMapper.java b/server-spi-private/src/main/java/org/keycloak/protocol/ProtocolMapper.java index 8055fae82da..8ee0600b0cc 100755 --- a/server-spi-private/src/main/java/org/keycloak/protocol/ProtocolMapper.java +++ b/server-spi-private/src/main/java/org/keycloak/protocol/ProtocolMapper.java @@ -34,6 +34,14 @@ public interface ProtocolMapper extends Provider, ProviderFactoryBill Burke @@ -58,6 +66,21 @@ public class ProtocolMapperUtils { public static final String MULTIVALUED_LABEL = "multivalued.label"; public static final String MULTIVALUED_HELP_TEXT = "multivalued.tooltip"; + // Role name mapper can move some roles to different positions + public static final int PRIORITY_ROLE_NAMES_MAPPER = 10; + + // Hardcoded role mapper can be used to add some roles + public static final int PRIORITY_HARDCODED_ROLE_MAPPER = 20; + + // Audiences can be resolved once all the roles are correctly set + public static final int PRIORITY_AUDIENCE_RESOLVE_MAPPER = 30; + + // Add roles to tokens finally + public static final int PRIORITY_ROLE_MAPPER = 40; + + // Script mapper goes last, so it can access the roles in the token + public static final int PRIORITY_SCRIPT_MAPPER = 50; + public static String getUserModelValue(UserModel user, String propertyName) { String methodName = "get" + Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1); @@ -95,4 +118,31 @@ public class ProtocolMapperUtils { } return null; } + + + public static List> getSortedProtocolMappers(KeycloakSession session, ClientSessionContext ctx) { + Set mapperModels = ctx.getProtocolMappers(); + Map result = new HashMap<>(); + + KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory(); + for (ProtocolMapperModel mapperModel : mapperModels) { + ProtocolMapper mapper = (ProtocolMapper) sessionFactory.getProviderFactory(ProtocolMapper.class, mapperModel.getProtocolMapper()); + if (mapper == null) { + continue; + } + + result.put(mapperModel, mapper); + } + + return result.entrySet() + .stream() + .sorted(Comparator.comparing(ProtocolMapperUtils::compare)) + .collect(Collectors.toList()); + } + + public static int compare(Map.Entry entry) { + int priority = entry.getValue().getPriority(); + return priority; + } + } diff --git a/services/src/main/java/org/keycloak/protocol/docker/DockerAuthV2Protocol.java b/services/src/main/java/org/keycloak/protocol/docker/DockerAuthV2Protocol.java index fc42d8d52ad..8c3c5bd856d 100644 --- a/services/src/main/java/org/keycloak/protocol/docker/DockerAuthV2Protocol.java +++ b/services/src/main/java/org/keycloak/protocol/docker/DockerAuthV2Protocol.java @@ -16,6 +16,7 @@ import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.ProtocolMapper; +import org.keycloak.protocol.ProtocolMapperUtils; import org.keycloak.protocol.docker.mapper.DockerAuthV2AttributeMapper; import org.keycloak.representations.docker.DockerResponse; import org.keycloak.representations.docker.DockerResponseToken; @@ -30,6 +31,7 @@ import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; import java.text.SimpleDateFormat; import java.util.Date; +import java.util.Map; import java.util.Set; public class DockerAuthV2Protocol implements LoginProtocol { @@ -110,9 +112,11 @@ public class DockerAuthV2Protocol implements LoginProtocol { .expiration(responseToken.getIssuedAt() + accessTokenLifespan); // Next, allow mappers to decorate the token to add/remove scopes as appropriate - final Set mappings = clientSessionCtx.getProtocolMappers(); - for (final ProtocolMapperModel mapping : mappings) { - final ProtocolMapper mapper = (ProtocolMapper) session.getKeycloakSessionFactory().getProviderFactory(ProtocolMapper.class, mapping.getProtocolMapper()); + + for (Map.Entry entry : ProtocolMapperUtils.getSortedProtocolMappers(session, clientSessionCtx)) { + ProtocolMapperModel mapping = entry.getKey(); + ProtocolMapper mapper = entry.getValue(); + if (mapper instanceof DockerAuthV2AttributeMapper) { final DockerAuthV2AttributeMapper dockerAttributeMapper = (DockerAuthV2AttributeMapper) mapper; if (dockerAttributeMapper.appliesTo(responseToken)) { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolFactory.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolFactory.java index ca4ddc482a5..f5371e33389 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolFactory.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolFactory.java @@ -33,22 +33,20 @@ import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.AbstractLoginProtocolFactory; import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.oidc.mappers.AddressMapper; +import org.keycloak.protocol.oidc.mappers.AllowedWebOriginsProtocolMapper; +import org.keycloak.protocol.oidc.mappers.AudienceResolveProtocolMapper; import org.keycloak.protocol.oidc.mappers.FullNameMapper; -import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper; import org.keycloak.protocol.oidc.mappers.UserAttributeMapper; +import org.keycloak.protocol.oidc.mappers.UserClientRoleMappingMapper; import org.keycloak.protocol.oidc.mappers.UserPropertyMapper; +import org.keycloak.protocol.oidc.mappers.UserRealmRoleMappingMapper; import org.keycloak.protocol.oidc.mappers.UserSessionNoteMapper; import org.keycloak.representations.IDToken; import org.keycloak.representations.idm.ClientRepresentation; -import org.keycloak.representations.idm.ClientScopeRepresentation; import org.keycloak.services.ServicesLogger; -import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; import java.util.Map; import java.util.Set; @@ -78,12 +76,20 @@ public class OIDCLoginProtocolFactory extends AbstractLoginProtocolFactory { public static final String ADDRESS = "address"; public static final String PHONE_NUMBER = "phone number"; public static final String PHONE_NUMBER_VERIFIED = "phone number verified"; + public static final String REALM_ROLES = "realm roles"; + public static final String CLIENT_ROLES = "client roles"; + public static final String AUDIENCE_RESOLVE = "audience resolve"; + public static final String ALLOWED_WEB_ORIGINS = "allowed web origins"; + + public static final String ROLES_SCOPE = "roles"; + public static final String WEB_ORIGINS_SCOPE = "web-origins"; public static final String PROFILE_SCOPE_CONSENT_TEXT = "${profileScopeConsentText}"; public static final String EMAIL_SCOPE_CONSENT_TEXT = "${emailScopeConsentText}"; public static final String ADDRESS_SCOPE_CONSENT_TEXT = "${addressScopeConsentText}"; public static final String PHONE_SCOPE_CONSENT_TEXT = "${phoneScopeConsentText}"; public static final String OFFLINE_ACCESS_SCOPE_CONSENT_TEXT = Constants.OFFLINE_ACCESS_SCOPE_CONSENT_TEXT; + public static final String ROLES_SCOPE_CONSENT_TEXT = "${rolesScopeConsentText}"; @Override @@ -155,6 +161,18 @@ public class OIDCLoginProtocolFactory extends AbstractLoginProtocolFactory { KerberosConstants.GSS_DELEGATION_CREDENTIAL, "String", true, false); builtins.put(KerberosConstants.GSS_DELEGATION_CREDENTIAL, model); + + model = UserRealmRoleMappingMapper.create(null, REALM_ROLES, "realm_access.roles", true, false, true); + builtins.put(REALM_ROLES, model); + + model = UserClientRoleMappingMapper.create(null, null, CLIENT_ROLES, "resource_access.${client_id}.roles", true, false, true); + builtins.put(CLIENT_ROLES, model); + + model = AudienceResolveProtocolMapper.createClaimMapper(AUDIENCE_RESOLVE); + builtins.put(AUDIENCE_RESOLVE, model); + + model = AllowedWebOriginsProtocolMapper.createClaimMapper(ALLOWED_WEB_ORIGINS); + builtins.put(ALLOWED_WEB_ORIGINS, model); } private static void createUserAttributeMapper(String name, String attrName, String claimName, String type) { @@ -172,6 +190,7 @@ public class OIDCLoginProtocolFactory extends AbstractLoginProtocolFactory { profileScope.setDescription("OpenID Connect built-in scope: profile"); profileScope.setDisplayOnConsentScreen(true); profileScope.setConsentScreenText(PROFILE_SCOPE_CONSENT_TEXT); + profileScope.setIncludeInTokenScope(true); profileScope.setProtocol(getId()); profileScope.addProtocolMapper(builtins.get(FULL_NAME)); profileScope.addProtocolMapper(builtins.get(FAMILY_NAME)); @@ -192,6 +211,7 @@ public class OIDCLoginProtocolFactory extends AbstractLoginProtocolFactory { emailScope.setDescription("OpenID Connect built-in scope: email"); emailScope.setDisplayOnConsentScreen(true); emailScope.setConsentScreenText(EMAIL_SCOPE_CONSENT_TEXT); + emailScope.setIncludeInTokenScope(true); emailScope.setProtocol(getId()); emailScope.addProtocolMapper(builtins.get(EMAIL)); emailScope.addProtocolMapper(builtins.get(EMAIL_VERIFIED)); @@ -200,6 +220,7 @@ public class OIDCLoginProtocolFactory extends AbstractLoginProtocolFactory { addressScope.setDescription("OpenID Connect built-in scope: address"); addressScope.setDisplayOnConsentScreen(true); addressScope.setConsentScreenText(ADDRESS_SCOPE_CONSENT_TEXT); + addressScope.setIncludeInTokenScope(true); addressScope.setProtocol(getId()); addressScope.addProtocolMapper(builtins.get(ADDRESS)); @@ -207,6 +228,7 @@ public class OIDCLoginProtocolFactory extends AbstractLoginProtocolFactory { phoneScope.setDescription("OpenID Connect built-in scope: phone"); phoneScope.setDisplayOnConsentScreen(true); phoneScope.setConsentScreenText(PHONE_SCOPE_CONSENT_TEXT); + phoneScope.setIncludeInTokenScope(true); phoneScope.setProtocol(getId()); phoneScope.addProtocolMapper(builtins.get(PHONE_NUMBER)); phoneScope.addProtocolMapper(builtins.get(PHONE_NUMBER_VERIFIED)); @@ -224,8 +246,56 @@ public class OIDCLoginProtocolFactory extends AbstractLoginProtocolFactory { DefaultClientScopes.createOfflineAccessClientScope(newRealm, offlineRole); } } + + addRolesClientScope(newRealm); + addWebOriginsClientScope(newRealm); } + + public static ClientScopeModel addRolesClientScope(RealmModel newRealm) { + ClientScopeModel rolesScope = KeycloakModelUtils.getClientScopeByName(newRealm, ROLES_SCOPE); + if (rolesScope == null) { + rolesScope = newRealm.addClientScope(ROLES_SCOPE); + rolesScope.setDescription("OpenID Connect scope for add user roles to the access token"); + rolesScope.setDisplayOnConsentScreen(true); + rolesScope.setConsentScreenText(ROLES_SCOPE_CONSENT_TEXT); + rolesScope.setIncludeInTokenScope(false); + rolesScope.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + rolesScope.addProtocolMapper(builtins.get(REALM_ROLES)); + rolesScope.addProtocolMapper(builtins.get(CLIENT_ROLES)); + rolesScope.addProtocolMapper(builtins.get(AUDIENCE_RESOLVE)); + + // 'roles' will be default client scope + newRealm.addDefaultClientScope(rolesScope, true); + } else { + logger.debugf("Client scope '%s' already exists in realm '%s'. Skip creating it.", ROLES_SCOPE, newRealm.getName()); + } + + return rolesScope; + } + + + public static ClientScopeModel addWebOriginsClientScope(RealmModel newRealm) { + ClientScopeModel originsScope = KeycloakModelUtils.getClientScopeByName(newRealm, WEB_ORIGINS_SCOPE); + if (originsScope == null) { + originsScope = newRealm.addClientScope(WEB_ORIGINS_SCOPE); + originsScope.setDescription("OpenID Connect scope for add allowed web origins to the access token"); + originsScope.setDisplayOnConsentScreen(false); // No requesting consent from user for this. It is rather the permission of client + originsScope.setConsentScreenText(""); + originsScope.setIncludeInTokenScope(false); + originsScope.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + originsScope.addProtocolMapper(builtins.get(ALLOWED_WEB_ORIGINS)); + + // 'web-origins' will be default client scope + newRealm.addDefaultClientScope(originsScope, true); + } else { + logger.debugf("Client scope '%s' already exists in realm '%s'. Skip creating it.", WEB_ORIGINS_SCOPE, newRealm.getName()); + } + + return originsScope; + } + + @Override protected void addDefaults(ClientModel client) { } 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 9ff893c294f..d5ee3e9fc81 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java @@ -47,6 +47,7 @@ import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionProvider; import org.keycloak.models.utils.KeycloakModelUtils; 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; @@ -414,11 +415,7 @@ public class TokenManager { public AccessToken createClientAccessToken(KeycloakSession session, RealmModel realm, ClientModel client, UserModel user, UserSessionModel userSession, ClientSessionContext clientSessionCtx) { - Set requestedRoles = clientSessionCtx.getRoles(); AccessToken token = initToken(realm, client, user, userSession, clientSessionCtx, session.getContext().getUri()); - for (RoleModel role : requestedRoles) { - addComposites(token, role); - } token = transformAccessToken(session, token, userSession, clientSessionCtx); return token; } @@ -597,13 +594,12 @@ public class TokenManager { public AccessToken transformAccessToken(KeycloakSession session, AccessToken token, UserSessionModel userSession, ClientSessionContext clientSessionCtx) { - Set mappings = clientSessionCtx.getProtocolMappers(); - KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory(); - for (ProtocolMapperModel mapping : mappings) { - ProtocolMapper mapper = (ProtocolMapper)sessionFactory.getProviderFactory(ProtocolMapper.class, mapping.getProtocolMapper()); + for (Map.Entry entry : ProtocolMapperUtils.getSortedProtocolMappers(session, clientSessionCtx)) { + ProtocolMapperModel mapping = entry.getKey(); + ProtocolMapper mapper = entry.getValue(); if (mapper instanceof OIDCAccessTokenMapper) { - token = ((OIDCAccessTokenMapper) mapper).transformAccessToken(token, mapping, session, userSession, clientSessionCtx.getClientSession()); + token = ((OIDCAccessTokenMapper) mapper).transformAccessToken(token, mapping, session, userSession, clientSessionCtx); } } @@ -612,13 +608,13 @@ public class TokenManager { public AccessToken transformUserInfoAccessToken(KeycloakSession session, AccessToken token, UserSessionModel userSession, ClientSessionContext clientSessionCtx) { - Set mappings = clientSessionCtx.getProtocolMappers(); - KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory(); - for (ProtocolMapperModel mapping : mappings) { - ProtocolMapper mapper = (ProtocolMapper)sessionFactory.getProviderFactory(ProtocolMapper.class, mapping.getProtocolMapper()); + for (Map.Entry entry : ProtocolMapperUtils.getSortedProtocolMappers(session, clientSessionCtx)) { + ProtocolMapperModel mapping = entry.getKey(); + ProtocolMapper mapper = entry.getValue(); + if (mapper instanceof UserInfoTokenMapper) { - token = ((UserInfoTokenMapper) mapper).transformUserInfoToken(token, mapping, session, userSession, clientSessionCtx.getClientSession()); + token = ((UserInfoTokenMapper) mapper).transformUserInfoToken(token, mapping, session, userSession, clientSessionCtx); } } @@ -627,13 +623,13 @@ public class TokenManager { public void transformIDToken(KeycloakSession session, IDToken token, UserSessionModel userSession, ClientSessionContext clientSessionCtx) { - Set mappings = clientSessionCtx.getProtocolMappers(); - KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory(); - for (ProtocolMapperModel mapping : mappings) { - ProtocolMapper mapper = (ProtocolMapper)sessionFactory.getProviderFactory(ProtocolMapper.class, mapping.getProtocolMapper()); + for (Map.Entry entry : ProtocolMapperUtils.getSortedProtocolMappers(session, clientSessionCtx)) { + ProtocolMapperModel mapping = entry.getKey(); + ProtocolMapper mapper = entry.getValue(); + if (mapper instanceof OIDCIDTokenMapper) { - token = ((OIDCIDTokenMapper) mapper).transformIDToken(token, mapping, session, userSession, clientSessionCtx.getClientSession()); + token = ((OIDCIDTokenMapper) mapper).transformIDToken(token, mapping, session, userSession, clientSessionCtx); } } } @@ -667,10 +663,6 @@ public class TokenManager { token.setSessionState(session.getId()); token.expiration(getTokenExpiration(realm, client, session, clientSession)); - Set allowedOrigins = client.getWebOrigins(); - if (allowedOrigins != null) { - token.setAllowedOrigins(WebOriginsUtils.resolveValidWebOrigins(uriInfo, client)); - } return token; } @@ -709,33 +701,6 @@ public class TokenManager { return expiration; } - protected void addComposites(AccessToken token, RoleModel role) { - AccessToken.Access access = null; - if (role.getContainer() instanceof RealmModel) { - access = token.getRealmAccess(); - if (token.getRealmAccess() == null) { - access = new AccessToken.Access(); - token.setRealmAccess(access); - } else if (token.getRealmAccess().getRoles() != null && token.getRealmAccess().isUserInRole(role.getName())) - return; - - } else { - ClientModel app = (ClientModel) role.getContainer(); - access = token.getResourceAccess(app.getClientId()); - if (access == null) { - access = token.addAccess(app.getClientId()); - if (app.isSurrogateAuthRequired()) access.verifyCaller(true); - } else if (access.isUserInRole(role.getName())) return; - - } - access.addRole(role.getName()); - if (!role.isComposite()) return; - - for (RoleModel composite : role.getComposites()) { - addComposites(token, composite); - } - - } public AccessTokenResponseBuilder responseBuilder(RealmModel realm, ClientModel client, EventBuilder event, KeycloakSession session, UserSessionModel userSession, ClientSessionContext clientSessionCtx) { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractOIDCProtocolMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractOIDCProtocolMapper.java index a7eeb5d82bd..b78d553a559 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractOIDCProtocolMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractOIDCProtocolMapper.java @@ -18,7 +18,7 @@ package org.keycloak.protocol.oidc.mappers; import org.keycloak.Config; -import org.keycloak.models.AuthenticatedClientSessionModel; +import org.keycloak.models.ClientSessionContext; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.ProtocolMapperModel; @@ -61,35 +61,35 @@ public abstract class AbstractOIDCProtocolMapper implements ProtocolMapper { } public AccessToken transformUserInfoToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, - UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { + UserSessionModel userSession, ClientSessionContext clientSessionCtx) { if (!OIDCAttributeMapperHelper.includeInUserInfo(mappingModel)) { return token; } - setClaim(token, mappingModel, userSession, session); + setClaim(token, mappingModel, userSession, session, clientSessionCtx); return token; } public AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, - UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { + UserSessionModel userSession, ClientSessionContext clientSessionCtx) { if (!OIDCAttributeMapperHelper.includeInAccessToken(mappingModel)){ return token; } - setClaim(token, mappingModel, userSession, session); + setClaim(token, mappingModel, userSession, session, clientSessionCtx); return token; } public IDToken transformIDToken(IDToken token, ProtocolMapperModel mappingModel, KeycloakSession session, - UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { + UserSessionModel userSession, ClientSessionContext clientSessionCtx) { if (!OIDCAttributeMapperHelper.includeInIDToken(mappingModel)){ return token; } - setClaim(token, mappingModel, userSession, session); + setClaim(token, mappingModel, userSession, session, clientSessionCtx); return token; } @@ -99,7 +99,7 @@ public abstract class AbstractOIDCProtocolMapper implements ProtocolMapper { * @param mappingModel * @param userSession * - * @deprecated override {@link #setClaim(IDToken, ProtocolMapperModel, UserSessionModel, KeycloakSession)} instead. + * @deprecated override {@link #setClaim(IDToken, ProtocolMapperModel, UserSessionModel, KeycloakSession, ClientSessionContext)} instead. */ @Deprecated protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession) { @@ -111,8 +111,10 @@ public abstract class AbstractOIDCProtocolMapper implements ProtocolMapper { * @param mappingModel * @param userSession * @param keycloakSession + * @param clientSessionCtx */ - protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession, KeycloakSession keycloakSession) { + protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession, KeycloakSession keycloakSession, + ClientSessionContext clientSessionCtx) { // we delegate to the old #setClaim(...) method for backwards compatibility setClaim(token, mappingModel, userSession); } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractPairwiseSubMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractPairwiseSubMapper.java index 3f124e21fa4..fdc3433a67f 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractPairwiseSubMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractPairwiseSubMapper.java @@ -2,6 +2,7 @@ package org.keycloak.protocol.oidc.mappers; import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientSessionContext; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperContainerModel; import org.keycloak.models.ProtocolMapperModel; @@ -64,20 +65,20 @@ public abstract class AbstractPairwiseSubMapper extends AbstractOIDCProtocolMapp } @Override - public IDToken transformIDToken(IDToken token, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { - setIDTokenSubject(token, generateSub(mappingModel, getSectorIdentifier(clientSession.getClient(), mappingModel), userSession.getUser().getId())); + public IDToken transformIDToken(IDToken token, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionContext clientSessionCtx) { + setIDTokenSubject(token, generateSub(mappingModel, getSectorIdentifier(clientSessionCtx.getClientSession().getClient(), mappingModel), userSession.getUser().getId())); return token; } @Override - public AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { - setAccessTokenSubject(token, generateSub(mappingModel, getSectorIdentifier(clientSession.getClient(), mappingModel), userSession.getUser().getId())); + public AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionContext clientSessionCtx) { + setAccessTokenSubject(token, generateSub(mappingModel, getSectorIdentifier(clientSessionCtx.getClientSession().getClient(), mappingModel), userSession.getUser().getId())); return token; } @Override - public AccessToken transformUserInfoToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { - setUserInfoTokenSubject(token, generateSub(mappingModel, getSectorIdentifier(clientSession.getClient(), mappingModel), userSession.getUser().getId())); + public AccessToken transformUserInfoToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionContext clientSessionCtx) { + setUserInfoTokenSubject(token, generateSub(mappingModel, getSectorIdentifier(clientSessionCtx.getClientSession().getClient(), mappingModel), userSession.getUser().getId())); return token; } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractUserRoleMappingMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractUserRoleMappingMapper.java index d239951bf9b..e9333379fb4 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractUserRoleMappingMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractUserRoleMappingMapper.java @@ -17,20 +17,18 @@ package org.keycloak.protocol.oidc.mappers; -import org.keycloak.models.GroupModel; import org.keycloak.models.ProtocolMapperModel; -import org.keycloak.models.RoleModel; -import org.keycloak.models.UserModel; -import org.keycloak.models.UserSessionModel; -import org.keycloak.models.utils.RoleUtils; import org.keycloak.protocol.ProtocolMapperUtils; +import org.keycloak.representations.AccessToken; import org.keycloak.representations.IDToken; +import java.util.Collection; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Set; -import java.util.function.Predicate; +import java.util.regex.Pattern; import java.util.stream.Collectors; -import java.util.stream.Stream; /** * Base class for mapping of user role mappings to an ID and Access Token claim. @@ -39,38 +37,11 @@ import java.util.stream.Stream; */ abstract class AbstractUserRoleMappingMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper, OIDCIDTokenMapper, UserInfoTokenMapper { - /** - * Returns a stream with roles that come from: - *
    - *
  • Direct assignment of the role to the user
  • - *
  • Direct assignment of the role to any group of the user or any of its parent group
  • - *
  • Composite roles are expanded recursively, the composite role itself is also contained in the returned stream
  • - *
- * @param user User to enumerate the roles for - * @return - */ - public static Stream getAllUserRolesStream(UserModel user) { - return Stream.concat( - user.getRoleMappings().stream(), - user.getGroups().stream() - .flatMap(g -> groupAndItsParentsStream(g)) - .flatMap(g -> g.getRoleMappings().stream())) - .flatMap(RoleUtils::expandCompositeRolesStream); + @Override + public int getPriority() { + return ProtocolMapperUtils.PRIORITY_ROLE_MAPPER; } - /** - * Returns stream of the given group and its parents (recursively). - * @param group - * @return - */ - private static Stream groupAndItsParentsStream(GroupModel group) { - Stream.Builder sb = Stream.builder(); - while (group != null) { - sb.add(group); - group = group.getParent(); - } - return sb.build(); - } /** * Retrieves all roles of the current user based on direct roles set to the user, its groups and their parent groups. @@ -81,31 +52,22 @@ abstract class AbstractUserRoleMappingMapper extends AbstractOIDCProtocolMapper * * @param token * @param mappingModel - * @param userSession - * @param restriction + * @param rolesToAdd + * @param clientId * @param prefix */ - protected static void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession, - Predicate restriction, String prefix) { - String rolePrefix = prefix == null ? "" : prefix; - UserModel user = userSession.getUser(); + protected static void setClaim(IDToken token, ProtocolMapperModel mappingModel, Set rolesToAdd, + String clientId, String prefix) { - // get a set of all realm roles assigned to the user or its group - Stream clientUserRoles = getAllUserRolesStream(user).filter(restriction); - - boolean dontLimitScope = userSession.getAuthenticatedClientSessions().values().stream().anyMatch(cs -> cs.getClient().isFullScopeAllowed()); - if (! dontLimitScope) { - Set clientRoles = userSession.getAuthenticatedClientSessions().values().stream() - .flatMap(cs -> cs.getClient().getScopeMappings().stream()) - .collect(Collectors.toSet()); - - clientUserRoles = clientUserRoles.filter(clientRoles::contains); + Set realmRoleNames; + if (prefix != null && !prefix.isEmpty()) { + realmRoleNames = rolesToAdd.stream() + .map(roleName -> prefix + roleName) + .collect(Collectors.toSet()); + } else { + realmRoleNames = rolesToAdd; } - List realmRoleNames = clientUserRoles - .map(m -> rolePrefix + m.getName()) - .collect(Collectors.toList()); - Object claimValue = realmRoleNames; boolean multiValued = "true".equals(mappingModel.getConfig().get(ProtocolMapperUtils.MULTIVALUED)); @@ -113,6 +75,91 @@ abstract class AbstractUserRoleMappingMapper extends AbstractOIDCProtocolMapper claimValue = realmRoleNames.toString(); } - OIDCAttributeMapperHelper.mapClaim(token, mappingModel, claimValue); + //OIDCAttributeMapperHelper.mapClaim(token, mappingModel, claimValue); + mapClaim(token, mappingModel, claimValue, clientId); + } + + + private static final Pattern CLIENT_ID_PATTERN = Pattern.compile("\\$\\{client_id\\}"); + + private static void mapClaim(IDToken token, ProtocolMapperModel mappingModel, Object attributeValue, String clientId) { + attributeValue = OIDCAttributeMapperHelper.mapAttributeValue(mappingModel, attributeValue); + if (attributeValue == null) return; + + String protocolClaim = mappingModel.getConfig().get(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME); + if (protocolClaim == null) { + return; + } + + if (clientId != null) { + protocolClaim = CLIENT_ID_PATTERN.matcher(protocolClaim).replaceAll(clientId); + } + + List split = OIDCAttributeMapperHelper.splitClaimPath(protocolClaim); + + // Special case + if (checkAccessToken(token, split, attributeValue)) { + return; + } + + final int length = split.size(); + int i = 0; + Map jsonObject = token.getOtherClaims(); + for (String component : split) { + i++; + if (i == length) { + // Case when we want to add to existing set of roles + Object last = jsonObject.get(component); + if (last != null && last instanceof Collection && attributeValue instanceof Collection) { + ((Collection) last).addAll((Collection) attributeValue); + } else { + jsonObject.put(component, attributeValue); + } + + } else { + Map nested = (Map)jsonObject.get(component); + + if (nested == null) { + nested = new HashMap<>(); + jsonObject.put(component, nested); + } + + jsonObject = nested; + } + } + } + + + // Special case when roles are put to the access token via "realmAcces, resourceAccess" properties + private static boolean checkAccessToken(IDToken idToken, List path, Object attributeValue) { + if (!(idToken instanceof AccessToken)) { + return false; + } + + if (!(attributeValue instanceof Collection)) { + return false; + } + + Collection roles = (Collection) attributeValue; + + AccessToken token = (AccessToken) idToken; + AccessToken.Access access = null; + if (path.size() == 2 && "realm_access".equals(path.get(0)) && "roles".equals(path.get(1))) { + access = token.getRealmAccess(); + if (access == null) { + access = new AccessToken.Access(); + token.setRealmAccess(access); + } + } else if (path.size() == 3 && "resource_access".equals(path.get(0)) && "roles".equals(path.get(2))) { + String clientId = path.get(1); + access = token.addAccess(clientId); + } else { + return false; + } + + for (String role : roles) { + access.addRole(role); + } + return true; } } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AllowedWebOriginsProtocolMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AllowedWebOriginsProtocolMapper.java new file mode 100644 index 00000000000..65c004416a8 --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AllowedWebOriginsProtocolMapper.java @@ -0,0 +1,98 @@ +/* + * Copyright 2017 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 java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import javax.ws.rs.core.UriInfo; + +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientSessionContext; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.ProtocolMapperModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.protocol.oidc.utils.WebOriginsUtils; +import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.representations.AccessToken; + +/** + * Protocol mapper to add allowed web origins to the access token to the 'allowed-origins' claim + * + * @author Marek Posolda + */ +public class AllowedWebOriginsProtocolMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper { + + private static final List configProperties = new ArrayList(); + + + public static final String PROVIDER_ID = "oidc-allowed-origins-mapper"; + + + public List getConfigProperties() { + return configProperties; + } + + @Override + public String getId() { + return PROVIDER_ID; + } + + @Override + public String getDisplayType() { + return "Allowed Web Origins"; + } + + @Override + public String getDisplayCategory() { + return TOKEN_MAPPER_CATEGORY; + } + + @Override + public String getHelpText() { + return "Adds all allowed web origins to the 'allowed-origins' claim in the token"; + } + + @Override + public AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, + UserSessionModel userSession, ClientSessionContext clientSessionCtx) { + ClientModel client = clientSessionCtx.getClientSession().getClient(); + UriInfo uriInfo = session.getContext().getUri(); + + Set allowedOrigins = client.getWebOrigins(); + if (allowedOrigins != null && !allowedOrigins.isEmpty()) { + token.setAllowedOrigins(WebOriginsUtils.resolveValidWebOrigins(uriInfo, client)); + } + + return token; + } + + + public static ProtocolMapperModel createClaimMapper(String name) { + ProtocolMapperModel mapper = new ProtocolMapperModel(); + mapper.setName(name); + mapper.setProtocolMapper(PROVIDER_ID); + mapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + mapper.setConfig(Collections.emptyMap()); + return mapper; + } + +} diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AudienceProtocolMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AudienceProtocolMapper.java index bf63858d46c..dcd0f981d24 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AudienceProtocolMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AudienceProtocolMapper.java @@ -22,6 +22,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import org.keycloak.models.ClientSessionContext; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.UserSessionModel; @@ -98,7 +99,8 @@ public class AudienceProtocolMapper extends AbstractOIDCProtocolMapper implement return "Add specified audience to the audience (aud) field of token"; } - protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession, KeycloakSession keycloakSession) { + @Override + protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession, KeycloakSession keycloakSession, ClientSessionContext clientSessionCtx) { String audienceValue = mappingModel.getConfig().get(INCLUDED_CLIENT_AUDIENCE); if (audienceValue == null) { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AudienceResolveProtocolMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AudienceResolveProtocolMapper.java new file mode 100644 index 00000000000..a50861c48b6 --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AudienceResolveProtocolMapper.java @@ -0,0 +1,100 @@ +/* + * Copyright 2017 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 java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.keycloak.models.ClientSessionContext; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.ProtocolMapperModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.protocol.ProtocolMapperUtils; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.representations.AccessToken; +import org.keycloak.utils.RoleResolveUtil; + +/** + * Protocol mapper, which adds all client_ids of "allowed" clients to the audience field of the token. Allowed client means the client + * for which user has at least one client role + * + * @author Marek Posolda + */ +public class AudienceResolveProtocolMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper { + + private static final List configProperties = new ArrayList(); + + + public static final String PROVIDER_ID = "oidc-audience-resolve-mapper"; + + + public List getConfigProperties() { + return configProperties; + } + + @Override + public String getId() { + return PROVIDER_ID; + } + + @Override + public String getDisplayType() { + return "Audience Resolve"; + } + + @Override + public String getDisplayCategory() { + return TOKEN_MAPPER_CATEGORY; + } + + @Override + public String getHelpText() { + return "Adds all client_ids of \"allowed\" clients to the audience field of the token. Allowed client means the client\n" + + " for which user has at least one client role"; + } + + @Override + public int getPriority() { + return ProtocolMapperUtils.PRIORITY_AUDIENCE_RESOLVE_MAPPER; + } + + @Override + public AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, + UserSessionModel userSession, ClientSessionContext clientSessionCtx) { + for (Map.Entry entry : RoleResolveUtil.getAllResolvedClientRoles(session, clientSessionCtx).entrySet()) { + AccessToken.Access access = entry.getValue(); + if (access != null && access.getRoles() != null && !access.getRoles().isEmpty()) { + token.addAudience(entry.getKey()); + } + } + + return token; + } + + public static ProtocolMapperModel createClaimMapper(String name) { + ProtocolMapperModel mapper = new ProtocolMapperModel(); + mapper.setName(name); + mapper.setProtocolMapper(PROVIDER_ID); + mapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + mapper.setConfig(Collections.emptyMap()); + return mapper; + } +} 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 19ff92559e5..4ab9eba4259 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 @@ -17,14 +17,16 @@ package org.keycloak.protocol.oidc.mappers; -import org.keycloak.models.AuthenticatedClientSessionModel; +import org.keycloak.models.ClientSessionContext; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.protocol.ProtocolMapperUtils; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.representations.AccessToken; +import org.keycloak.utils.RoleResolveUtil; import java.util.ArrayList; import java.util.HashMap; @@ -80,22 +82,24 @@ public class HardcodedRole extends AbstractOIDCProtocolMapper implements OIDCAcc return "Hardcode a role into the access token."; } + @Override + public int getPriority() { + return ProtocolMapperUtils.PRIORITY_HARDCODED_ROLE_MAPPER; + } + @Override public AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, - UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { + UserSessionModel userSession, ClientSessionContext clientSessionCtx) { String role = mappingModel.getConfig().get(ROLE_CONFIG); String[] scopedRole = KeycloakModelUtils.parseRole(role); String appName = scopedRole[0]; String roleName = scopedRole[1]; if (appName != null) { - token.addAccess(appName).addRole(roleName); + AccessToken.Access access = RoleResolveUtil.getResolvedClientRoles(session, clientSessionCtx, appName, true); + access.addRole(roleName); } else { - AccessToken.Access access = token.getRealmAccess(); - if (access == null) { - access = new AccessToken.Access(); - token.setRealmAccess(access); - } + AccessToken.Access access = RoleResolveUtil.getResolvedRealmRoles(session, clientSessionCtx, true); access.addRole(role); } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAccessTokenMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAccessTokenMapper.java index e7e0b7b4226..ff11ab937cc 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAccessTokenMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAccessTokenMapper.java @@ -18,6 +18,7 @@ package org.keycloak.protocol.oidc.mappers; import org.keycloak.models.AuthenticatedClientSessionModel; +import org.keycloak.models.ClientSessionContext; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.UserSessionModel; @@ -30,5 +31,5 @@ import org.keycloak.representations.AccessToken; public interface OIDCAccessTokenMapper { AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, - UserSessionModel userSession, AuthenticatedClientSessionModel clientSession); + UserSessionModel userSession, ClientSessionContext clientSessionCtx); } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAttributeMapperHelper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAttributeMapperHelper.java index 933654c845f..2ef45492778 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAttributeMapperHelper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAttributeMapperHelper.java @@ -56,8 +56,8 @@ public class OIDCAttributeMapperHelper { public static Object mapAttributeValue(ProtocolMapperModel mappingModel, Object attributeValue) { if (attributeValue == null) return null; - if (attributeValue instanceof List) { - List valueAsList = (List) attributeValue; + if (attributeValue instanceof Collection) { + Collection valueAsList = (Collection) attributeValue; if (valueAsList.isEmpty()) return null; if (isMultivalued(mappingModel)) { @@ -71,7 +71,7 @@ public class OIDCAttributeMapperHelper { ServicesLogger.LOGGER.multipleValuesForMapper(attributeValue.toString(), mappingModel.getName()); } - attributeValue = valueAsList.get(0); + attributeValue = valueAsList.iterator().next(); } } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCIDTokenMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCIDTokenMapper.java index 54f380b1656..a69f1dbd3a8 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCIDTokenMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCIDTokenMapper.java @@ -18,6 +18,7 @@ package org.keycloak.protocol.oidc.mappers; import org.keycloak.models.AuthenticatedClientSessionModel; +import org.keycloak.models.ClientSessionContext; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.UserSessionModel; @@ -30,5 +31,5 @@ import org.keycloak.representations.IDToken; public interface OIDCIDTokenMapper { IDToken transformIDToken(IDToken token, ProtocolMapperModel mappingModel, KeycloakSession session, - UserSessionModel userSession, AuthenticatedClientSessionModel clientSession); + UserSessionModel userSession, ClientSessionContext clientSession); } 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 d41063b72e2..9a41a29b0a3 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 @@ -17,14 +17,16 @@ package org.keycloak.protocol.oidc.mappers; -import org.keycloak.models.AuthenticatedClientSessionModel; +import org.keycloak.models.ClientSessionContext; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.protocol.ProtocolMapperUtils; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.representations.AccessToken; +import org.keycloak.utils.RoleResolveUtil; import java.util.ArrayList; import java.util.HashMap; @@ -87,9 +89,14 @@ public class RoleNameMapper extends AbstractOIDCProtocolMapper implements OIDCAc return "Map an assigned role to a new name or position in the token."; } + @Override + public int getPriority() { + return ProtocolMapperUtils.PRIORITY_ROLE_NAMES_MAPPER; + } + @Override public AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, - UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { + UserSessionModel userSession, ClientSessionContext clientSessionCtx) { String role = mappingModel.getConfig().get(ROLE_CONFIG); String newName = mappingModel.getConfig().get(NEW_ROLE_NAME); @@ -98,12 +105,12 @@ public class RoleNameMapper extends AbstractOIDCProtocolMapper implements OIDCAc String appName = scopedRole[0]; String roleName = scopedRole[1]; if (appName != null) { - AccessToken.Access access = token.getResourceAccess(appName); + AccessToken.Access access = RoleResolveUtil.getResolvedClientRoles(session, clientSessionCtx, appName, false); if (access == null) return token; if (!access.getRoles().contains(roleName)) return token; access.getRoles().remove(roleName); } else { - AccessToken.Access access = token.getRealmAccess(); + AccessToken.Access access = RoleResolveUtil.getResolvedRealmRoles(session, clientSessionCtx, false); if (access == null || !access.getRoles().contains(roleName)) return token; access.getRoles().remove(roleName); } @@ -112,13 +119,9 @@ public class RoleNameMapper extends AbstractOIDCProtocolMapper implements OIDCAc String newRoleName = newScopedRole[1]; AccessToken.Access access = null; if (newAppName == null) { - access = token.getRealmAccess(); - if (access == null) { - access = new AccessToken.Access(); - token.setRealmAccess(access); - } + access = RoleResolveUtil.getResolvedRealmRoles(session, clientSessionCtx, true); } else { - access = token.addAccess(newAppName); + access = RoleResolveUtil.getResolvedClientRoles(session, clientSessionCtx, newAppName, true); } access.addRole(newRoleName); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/ScriptBasedOIDCProtocolMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/ScriptBasedOIDCProtocolMapper.java index 72e833b9a18..bee4b88fd47 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/ScriptBasedOIDCProtocolMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/ScriptBasedOIDCProtocolMapper.java @@ -19,6 +19,7 @@ package org.keycloak.protocol.oidc.mappers; import org.jboss.logging.Logger; import org.keycloak.common.Profile; +import org.keycloak.models.ClientSessionContext; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperContainerModel; import org.keycloak.models.ProtocolMapperModel; @@ -120,7 +121,13 @@ public class ScriptBasedOIDCProtocolMapper extends AbstractOIDCProtocolMapper im return Profile.isFeatureEnabled(Profile.Feature.SCRIPTS); } - protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession, KeycloakSession keycloakSession) { + @Override + public int getPriority() { + return ProtocolMapperUtils.PRIORITY_SCRIPT_MAPPER; + } + + @Override + protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession, KeycloakSession keycloakSession, ClientSessionContext clientSessionCtx) { UserModel user = userSession.getUser(); String scriptSource = mappingModel.getConfig().get(SCRIPT); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserClientRoleMappingMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserClientRoleMappingMapper.java index db064fa3d67..ca57bd6e862 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserClientRoleMappingMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserClientRoleMappingMapper.java @@ -17,24 +17,19 @@ package org.keycloak.protocol.oidc.mappers; -import org.keycloak.OAuth2Constants; -import org.keycloak.models.AuthenticatedClientSessionModel; -import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientScopeModel; +import org.keycloak.models.ClientSessionContext; +import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; -import org.keycloak.models.RealmModel; -import org.keycloak.models.RoleModel; import org.keycloak.models.UserSessionModel; import org.keycloak.protocol.ProtocolMapperUtils; -import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.representations.AccessToken; import org.keycloak.representations.IDToken; +import org.keycloak.utils.RoleResolveUtil; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; -import java.util.Set; -import java.util.function.Predicate; +import java.util.Map; /** * Allows mapping of user client role mappings to an ID and Access Token claim. @@ -45,6 +40,8 @@ public class UserClientRoleMappingMapper extends AbstractUserRoleMappingMapper { public static final String PROVIDER_ID = "oidc-usermodel-client-role-mapper"; + private static final String TOKEN_CLAIM_NAME_TOOLTIP = "usermodel.clientRoleMapping.tokenClaimName.tooltip"; + private static final List CONFIG_PROPERTIES = new ArrayList<>(); static { @@ -68,10 +65,17 @@ public class UserClientRoleMappingMapper extends AbstractUserRoleMappingMapper { multiValued.setLabel(ProtocolMapperUtils.MULTIVALUED_LABEL); multiValued.setHelpText(ProtocolMapperUtils.MULTIVALUED_HELP_TEXT); multiValued.setType(ProviderConfigProperty.BOOLEAN_TYPE); - multiValued.setDefaultValue(false); + multiValued.setDefaultValue("true"); CONFIG_PROPERTIES.add(multiValued); OIDCAttributeMapperHelper.addAttributeConfig(CONFIG_PROPERTIES, UserClientRoleMappingMapper.class); + + // Alternative tooltip for the 'Token Claim Name' + for (ProviderConfigProperty prop : CONFIG_PROPERTIES) { + if (OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME.equals(prop.getName())) { + prop.setHelpText(TOKEN_CLAIM_NAME_TOOLTIP); + } + } } @Override @@ -100,47 +104,33 @@ public class UserClientRoleMappingMapper extends AbstractUserRoleMappingMapper { } @Override - protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession) { + protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession, KeycloakSession session, ClientSessionContext clientSessionCtx) { String clientId = mappingModel.getConfig().get(ProtocolMapperUtils.USER_MODEL_CLIENT_ROLE_MAPPING_CLIENT_ID); String rolePrefix = mappingModel.getConfig().get(ProtocolMapperUtils.USER_MODEL_CLIENT_ROLE_MAPPING_ROLE_PREFIX); - setClaim(token, mappingModel, userSession, getClientRoleFilter(clientId, userSession), rolePrefix); + if (clientId != null && !clientId.isEmpty()) { + AccessToken.Access access = RoleResolveUtil.getResolvedClientRoles(session, clientSessionCtx, clientId, false); + if (access == null) { + return; + } + + AbstractUserRoleMappingMapper.setClaim(token, mappingModel, access.getRoles(), clientId, rolePrefix); + } else { + // If clientId is not specified, we consider all clients + Map allAccess = RoleResolveUtil.getAllResolvedClientRoles(session, clientSessionCtx); + + for (Map.Entry entry : allAccess.entrySet()) { + String currClientId = entry.getKey(); + AccessToken.Access access = entry.getValue(); + if (access == null) { + continue; + } + + AbstractUserRoleMappingMapper.setClaim(token, mappingModel, access.getRoles(), currClientId, rolePrefix); + } + } } - private static Predicate getClientRoleFilter(String clientId, UserSessionModel userSession) { - if (clientId == null) { - return RoleModel::isClientRole; - } - - RealmModel clientRealm = userSession.getRealm(); - ClientModel client = clientRealm.getClientByClientId(clientId.trim()); - - if (client == null) { - return RoleModel::isClientRole; - } - - boolean fullScopeAllowed = client.isFullScopeAllowed(); - Set clientRoleMappings = client.getRoles(); - if (fullScopeAllowed) { - return clientRoleMappings::contains; - } - - Set scopeMappings = new HashSet<>(); - - // Add scope mappings of current client + all clientScopes of this client (including optional scopes if scope parameter matches) - String scopeParam = null; - AuthenticatedClientSessionModel authClientSession = userSession.getAuthenticatedClientSessionByClient(client.getId()); - if (authClientSession != null) { - scopeParam = authClientSession.getNote(OAuth2Constants.SCOPE); - } - - Set clientScopes = TokenManager.getRequestedClientScopes(scopeParam, client); - for (ClientScopeModel clientScope : clientScopes) { - scopeMappings.addAll(clientScope.getScopeMappings()); - } - - return role -> clientRoleMappings.contains(role) && scopeMappings.contains(role); - } public static ProtocolMapperModel create(String clientId, String clientRolePrefix, String name, @@ -156,7 +146,7 @@ public class UserClientRoleMappingMapper extends AbstractUserRoleMappingMapper { boolean accessToken, boolean idToken, boolean multiValued) { ProtocolMapperModel mapper = OIDCAttributeMapperHelper.createClaimMapper(name, "foo", tokenClaimName, "String", - accessToken, idToken, + accessToken, idToken, false, PROVIDER_ID); mapper.getConfig().put(ProtocolMapperUtils.MULTIVALUED, String.valueOf(multiValued)); @@ -164,4 +154,5 @@ public class UserClientRoleMappingMapper extends AbstractUserRoleMappingMapper { mapper.getConfig().put(ProtocolMapperUtils.USER_MODEL_CLIENT_ROLE_MAPPING_ROLE_PREFIX, clientRolePrefix); return mapper; } + } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserInfoTokenMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserInfoTokenMapper.java index e1fc17e9007..d3f80cb586a 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserInfoTokenMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserInfoTokenMapper.java @@ -18,6 +18,7 @@ package org.keycloak.protocol.oidc.mappers; import org.keycloak.models.AuthenticatedClientSessionModel; +import org.keycloak.models.ClientSessionContext; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.UserSessionModel; @@ -29,5 +30,5 @@ import org.keycloak.representations.AccessToken; public interface UserInfoTokenMapper { AccessToken transformUserInfoToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, - UserSessionModel userSession, AuthenticatedClientSessionModel clientSession); + UserSessionModel userSession, ClientSessionContext clientSessionCtx); } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserRealmRoleMappingMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserRealmRoleMappingMapper.java index 59eaf4be270..1f817d61242 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserRealmRoleMappingMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserRealmRoleMappingMapper.java @@ -17,11 +17,15 @@ package org.keycloak.protocol.oidc.mappers; +import org.keycloak.models.ClientSessionContext; +import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.UserSessionModel; import org.keycloak.protocol.ProtocolMapperUtils; import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.representations.AccessToken; import org.keycloak.representations.IDToken; +import org.keycloak.utils.RoleResolveUtil; import java.util.ArrayList; import java.util.List; @@ -51,7 +55,7 @@ public class UserRealmRoleMappingMapper extends AbstractUserRoleMappingMapper { multiValued.setLabel(ProtocolMapperUtils.MULTIVALUED_LABEL); multiValued.setHelpText(ProtocolMapperUtils.MULTIVALUED_HELP_TEXT); multiValued.setType(ProviderConfigProperty.BOOLEAN_TYPE); - multiValued.setDefaultValue(false); + multiValued.setDefaultValue("true"); CONFIG_PROPERTIES.add(multiValued); OIDCAttributeMapperHelper.addAttributeConfig(CONFIG_PROPERTIES, UserRealmRoleMappingMapper.class); @@ -83,9 +87,15 @@ public class UserRealmRoleMappingMapper extends AbstractUserRoleMappingMapper { } @Override - protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession) { + protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession, KeycloakSession session, ClientSessionContext clientSessionCtx) { String rolePrefix = mappingModel.getConfig().get(ProtocolMapperUtils.USER_MODEL_REALM_ROLE_MAPPING_ROLE_PREFIX); - AbstractUserRoleMappingMapper.setClaim(token, mappingModel, userSession, role -> ! role.isClientRole(), rolePrefix); + + AccessToken.Access access = RoleResolveUtil.getResolvedRealmRoles(session, clientSessionCtx, false); + if (access == null) { + return; + } + + AbstractUserRoleMappingMapper.setClaim(token, mappingModel, access.getRoles(),null, rolePrefix); } public static ProtocolMapperModel create(String realmRolePrefix, @@ -100,7 +110,7 @@ public class UserRealmRoleMappingMapper extends AbstractUserRoleMappingMapper { String tokenClaimName, boolean accessToken, boolean idToken, boolean multiValued) { ProtocolMapperModel mapper = OIDCAttributeMapperHelper.createClaimMapper(name, "foo", tokenClaimName, "String", - accessToken, idToken, + accessToken, idToken, false, PROVIDER_ID); mapper.getConfig().put(ProtocolMapperUtils.MULTIVALUED, String.valueOf(multiValued)); diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java b/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java index 52d271aa118..65a6059e610 100755 --- a/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java +++ b/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java @@ -43,6 +43,7 @@ import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.ProtocolMapper; +import org.keycloak.protocol.ProtocolMapperUtils; import org.keycloak.protocol.saml.mappers.SAMLAttributeStatementMapper; import org.keycloak.protocol.saml.mappers.SAMLLoginResponseMapper; import org.keycloak.protocol.saml.mappers.SAMLRoleListMapper; @@ -407,12 +408,10 @@ public class SamlProtocol implements LoginProtocol { List> loginResponseMappers = new LinkedList<>(); ProtocolMapperProcessor roleListMapper = null; - Set mappings = clientSessionCtx.getProtocolMappers(); - for (ProtocolMapperModel mapping : mappings) { + for (Map.Entry entry : ProtocolMapperUtils.getSortedProtocolMappers(session, clientSessionCtx)) { + ProtocolMapperModel mapping = entry.getKey(); + ProtocolMapper mapper = entry.getValue(); - ProtocolMapper mapper = (ProtocolMapper) session.getKeycloakSessionFactory().getProviderFactory(ProtocolMapper.class, mapping.getProtocolMapper()); - if (mapper == null) - continue; if (mapper instanceof SAMLAttributeStatementMapper) { attributeStatementMappers.add(new ProtocolMapperProcessor((SAMLAttributeStatementMapper) mapper, mapping)); } 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 835c24c9d62..dd70b538812 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 @@ -26,6 +26,7 @@ import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.RoleUtils; import org.keycloak.protocol.ProtocolMapper; +import org.keycloak.protocol.ProtocolMapperUtils; import org.keycloak.protocol.saml.SamlProtocol; import org.keycloak.provider.ProviderConfigProperty; @@ -114,13 +115,11 @@ public class RoleListMapper extends AbstractSAMLProtocolMapper implements SAMLRo boolean singleAttribute = Boolean.parseBoolean(single); List> roleNameMappers = new LinkedList<>(); - KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory(); AttributeType singleAttributeType = null; - Set requestedProtocolMappers = clientSessionCtx.getProtocolMappers(); - for (ProtocolMapperModel mapping : requestedProtocolMappers) { - ProtocolMapper mapper = (ProtocolMapper)sessionFactory.getProviderFactory(ProtocolMapper.class, mapping.getProtocolMapper()); - if (mapper == null) continue; + for (Map.Entry entry : ProtocolMapperUtils.getSortedProtocolMappers(session, clientSessionCtx)) { + ProtocolMapperModel mapping = entry.getKey(); + ProtocolMapper mapper = entry.getValue(); if (mapper instanceof SAMLRoleNameMapper) { roleNameMappers.add(new SamlProtocol.ProtocolMapperProcessor<>((SAMLRoleNameMapper) mapper,mapping)); diff --git a/services/src/main/java/org/keycloak/services/migration/DefaultMigrationProvider.java b/services/src/main/java/org/keycloak/services/migration/DefaultMigrationProvider.java index 747bdadc699..0c2e5f58fe0 100755 --- a/services/src/main/java/org/keycloak/services/migration/DefaultMigrationProvider.java +++ b/services/src/main/java/org/keycloak/services/migration/DefaultMigrationProvider.java @@ -19,6 +19,7 @@ package org.keycloak.services.migration; import org.keycloak.migration.MigrationProvider; import org.keycloak.models.ClaimMask; +import org.keycloak.models.ClientScopeModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RealmModel; @@ -83,6 +84,19 @@ public class DefaultMigrationProvider implements MigrationProvider { new RealmManager(session).setupAdminCli(realm); } + + @Override + public ClientScopeModel addOIDCRolesClientScope(RealmModel realm) { + return OIDCLoginProtocolFactory.addRolesClientScope(realm); + } + + + @Override + public ClientScopeModel addOIDCWebOriginsClientScope(RealmModel realm) { + return OIDCLoginProtocolFactory.addWebOriginsClientScope(realm); + } + + @Override public void close() { } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientScopesResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientScopesResource.java index 91fb49a21c7..1b948e8af3d 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientScopesResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientScopesResource.java @@ -156,6 +156,8 @@ public class ClientScopesResource { consentText = consentText.substring(0, 1).toUpperCase() + consentText.substring(1); clientScopeModel.setConsentScreenText(consentText); + clientScopeModel.setIncludeInTokenScope(true); + // Add audience protocol mapper ProtocolMapperModel audienceMapper = AudienceProtocolMapper.createClaimMapper("Audience for " + clientId, clientId, null,true, false); clientScopeModel.addProtocolMapper(audienceMapper); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java index 297bf973e52..fc106631917 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java @@ -257,6 +257,7 @@ public class ServerInfoAdminResource { rep.setName(mapper.getDisplayType()); rep.setHelpText(mapper.getHelpText()); rep.setCategory(mapper.getDisplayCategory()); + rep.setPriority(mapper.getPriority()); rep.setProperties(new LinkedList()); List configProperties = mapper.getConfigProperties(); rep.setProperties(ModelToRepresentation.toRepresentation(configProperties)); 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 8211698cb1e..debd7000e50 100644 --- a/services/src/main/java/org/keycloak/services/util/DefaultClientSessionContext.java +++ b/services/src/main/java/org/keycloak/services/util/DefaultClientSessionContext.java @@ -141,6 +141,10 @@ public class DefaultClientSessionContext implements ClientSessionContext { continue; } + if (!clientScope.isIncludeInTokenScope()) { + continue; + } + if (first) { first = false; } else { diff --git a/services/src/main/java/org/keycloak/utils/RoleResolveUtil.java b/services/src/main/java/org/keycloak/utils/RoleResolveUtil.java new file mode 100644 index 00000000000..5f095ece285 --- /dev/null +++ b/services/src/main/java/org/keycloak/utils/RoleResolveUtil.java @@ -0,0 +1,150 @@ +/* + * Copyright 2017 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.utils; + +import java.util.Map; +import java.util.Set; + +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientSessionContext; +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. + * Then all underlying protocolMappers can consume them. + * + * @author Marek Posolda + */ +public class RoleResolveUtil { + + private static final String RESOLVED_ROLES_ATTR = "RESOLVED_ROLES"; + + + /** + * Object (possibly null) containing all the user's realm roles. Including user's groups roles. Composite roles are expanded. + * Just the roles, which current client has role-scope-mapping for (or it's clientScopes) are included. + * Current client means the client corresponding to specified clientSessionCtx. + * + * @param session + * @param clientSessionCtx + * @param createIfMissing + * @return can return null (just in case that createIfMissing is false) + */ + public static AccessToken.Access getResolvedRealmRoles(KeycloakSession session, ClientSessionContext clientSessionCtx, boolean createIfMissing) { + AccessToken rolesToken = getAllCompositeRoles(session, clientSessionCtx); + AccessToken.Access access = rolesToken.getRealmAccess(); + if (access == null && createIfMissing) { + access = new AccessToken.Access(); + rolesToken.setRealmAccess(access); + } + + return access; + } + + + /** + * Object (possibly null) containing all the user's client roles of client specified by clientId. Including user's groups roles. + * Composite roles are expanded. Just the roles, which current client has role-scope-mapping for (or it's clientScopes) are included. + * Current client means the client corresponding to specified clientSessionCtx. + * + * @param session + * @param clientSessionCtx + * @param clientId + * @param createIfMissing + * @return can return null (just in case that createIfMissing is false) + */ + public static AccessToken.Access getResolvedClientRoles(KeycloakSession session, ClientSessionContext clientSessionCtx, String clientId, boolean createIfMissing) { + AccessToken rolesToken = getAllCompositeRoles(session, clientSessionCtx); + AccessToken.Access access = rolesToken.getResourceAccess(clientId); + + if (access == null && createIfMissing) { + access = rolesToken.addAccess(clientId); + } + + return access; + } + + + /** + * Object (but can be empty map) containing all the user's client roles of all clients. Including user's groups roles. Composite roles are expanded. + * Just the roles, which current client has role-scope-mapping for (or it's clientScopes) are included. + * Current client means the client corresponding to specified clientSessionCtx. + * + * @param session + * @param clientSessionCtx + * @return not-null object (can return empty map) + */ + public static Map getAllResolvedClientRoles(KeycloakSession session, ClientSessionContext clientSessionCtx) { + return getAllCompositeRoles(session, clientSessionCtx).getResourceAccess(); + } + + + private static AccessToken getAllCompositeRoles(KeycloakSession session, ClientSessionContext clientSessionCtx) { + AccessToken resolvedRoles = session.getAttribute(RESOLVED_ROLES_ATTR, AccessToken.class); + if (resolvedRoles == null) { + resolvedRoles = loadCompositeRoles(session, clientSessionCtx); + session.setAttribute(RESOLVED_ROLES_ATTR, resolvedRoles); + } + + return resolvedRoles; + } + + + private static AccessToken loadCompositeRoles(KeycloakSession session, ClientSessionContext clientSessionCtx) { + Set requestedRoles = clientSessionCtx.getRoles(); + AccessToken token = new AccessToken(); + for (RoleModel role : requestedRoles) { + addComposites(token, role); + } + return token; + } + + + private static void addComposites(AccessToken token, RoleModel role) { + AccessToken.Access access = null; + if (role.getContainer() instanceof RealmModel) { + access = token.getRealmAccess(); + if (token.getRealmAccess() == null) { + access = new AccessToken.Access(); + token.setRealmAccess(access); + } else if (token.getRealmAccess().getRoles() != null && token.getRealmAccess().isUserInRole(role.getName())) + return; + + } else { + ClientModel app = (ClientModel) role.getContainer(); + access = token.getResourceAccess(app.getClientId()); + if (access == null) { + access = token.addAccess(app.getClientId()); + if (app.isSurrogateAuthRequired()) access.verifyCaller(true); + } else if (access.isUserInRole(role.getName())) return; + + } + access.addRole(role.getName()); + if (!role.isComposite()) return; + + for (RoleModel composite : role.getComposites()) { + addComposites(token, composite); + } + + } + +} diff --git a/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper b/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper index a2ededd95b4..cf68931738f 100755 --- a/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper +++ b/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper @@ -25,6 +25,8 @@ org.keycloak.protocol.oidc.mappers.RoleNameMapper org.keycloak.protocol.oidc.mappers.UserSessionNoteMapper org.keycloak.protocol.oidc.mappers.GroupMembershipMapper org.keycloak.protocol.oidc.mappers.AudienceProtocolMapper +org.keycloak.protocol.oidc.mappers.AudienceResolveProtocolMapper +org.keycloak.protocol.oidc.mappers.AllowedWebOriginsProtocolMapper org.keycloak.protocol.saml.mappers.RoleListMapper org.keycloak.protocol.saml.mappers.RoleNameMapper org.keycloak.protocol.saml.mappers.HardcodedRole diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedClientStorageProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedClientStorageProvider.java index e53124aa363..a1e9edf64b7 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedClientStorageProvider.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedClientStorageProvider.java @@ -24,16 +24,19 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory; import org.keycloak.storage.StorageId; import org.keycloak.storage.client.AbstractReadOnlyClientStorageAdapter; import org.keycloak.storage.client.ClientLookupProvider; import org.keycloak.storage.client.ClientStorageProvider; import org.keycloak.storage.client.ClientStorageProviderModel; +import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; /** * @author Bill Burke @@ -219,7 +222,12 @@ public class HardcodedClientStorageProvider implements ClientStorageProvider, Cl @Override public Map getClientScopes(boolean defaultScope, boolean filterByProtocol) { if (defaultScope) { - return Collections.emptyMap(); + ClientScopeModel rolesScope = KeycloakModelUtils.getClientScopeByName(realm, OIDCLoginProtocolFactory.ROLES_SCOPE); + ClientScopeModel webOriginsScope = KeycloakModelUtils.getClientScopeByName(realm, OIDCLoginProtocolFactory.WEB_ORIGINS_SCOPE); + return Arrays.asList(rolesScope, webOriginsScope) + .stream() + .collect(Collectors.toMap(ClientScopeModel::getName, clientScope -> clientScope)); + } else { ClientScopeModel offlineScope = KeycloakModelUtils.getClientScopeByName(realm, "offline_access"); return Collections.singletonMap("offline_access", offlineScope); diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/OAuthGrantPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/OAuthGrantPage.java index a64df88b052..9bdf28a1698 100755 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/OAuthGrantPage.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/OAuthGrantPage.java @@ -16,9 +16,11 @@ */ package org.keycloak.testsuite.pages; +import java.util.Arrays; import java.util.LinkedList; import java.util.List; +import org.hamcrest.Matchers; import org.junit.Assert; import org.openqa.selenium.By; import org.openqa.selenium.WebElement; @@ -35,6 +37,7 @@ public class OAuthGrantPage extends LanguageComboboxAwarePage { public static final String ADDRESS_CONSENT_TEXT = "Address"; public static final String PHONE_CONSENT_TEXT = "Phone number"; public static final String OFFLINE_ACCESS_CONSENT_TEXT = "Offline Access"; + public static final String ROLES_CONSENT_TEXT = "User roles"; @FindBy(css = "input[name=\"accept\"]") private WebElement acceptButton; @@ -70,12 +73,11 @@ public class OAuthGrantPage extends LanguageComboboxAwarePage { } - public void assertGrants(String... grants) { + public void assertGrants(String... expectedGrants) { List displayed = getDisplayedGrants(); - Assert.assertEquals(displayed.size(), grants.length); - for (String grant : grants) { - Assert.assertTrue("Requested grant " + grant + " not present. Displayed grants: " + displayed, displayed.contains(grant)); - } + List expected = Arrays.asList(expectedGrants); + Assert.assertTrue("Not matched grants. Displayed grants: " + displayed + ", expected grants: " + expected, + displayed.containsAll(expected) && expected.containsAll(displayed)); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java index 5496790db78..17cd9b6ccea 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java @@ -642,6 +642,8 @@ public class ExportImportUtil { org.keycloak.testsuite.Assert.assertTrue(clientScopesMap.containsKey(OAuth2Constants.SCOPE_ADDRESS)); org.keycloak.testsuite.Assert.assertTrue(clientScopesMap.containsKey(OAuth2Constants.SCOPE_PHONE)); org.keycloak.testsuite.Assert.assertTrue(clientScopesMap.containsKey(OAuth2Constants.OFFLINE_ACCESS)); + org.keycloak.testsuite.Assert.assertTrue(clientScopesMap.containsKey(OIDCLoginProtocolFactory.ROLES_SCOPE)); + org.keycloak.testsuite.Assert.assertTrue(clientScopesMap.containsKey(OIDCLoginProtocolFactory.WEB_ORIGINS_SCOPE)); org.keycloak.testsuite.Assert.assertTrue(clientScopesMap.containsKey(SamlProtocolFactory.SCOPE_ROLE_LIST)); // Check content of some client scopes @@ -659,6 +661,8 @@ public class ExportImportUtil { .stream().collect(Collectors.toMap(clientScope -> clientScope.getName(), clientScope -> clientScope)); org.keycloak.testsuite.Assert.assertTrue(defaultClientScopes.containsKey(OAuth2Constants.SCOPE_PROFILE)); org.keycloak.testsuite.Assert.assertTrue(defaultClientScopes.containsKey(OAuth2Constants.SCOPE_EMAIL)); + org.keycloak.testsuite.Assert.assertTrue(defaultClientScopes.containsKey(OIDCLoginProtocolFactory.ROLES_SCOPE)); + org.keycloak.testsuite.Assert.assertTrue(defaultClientScopes.containsKey(OIDCLoginProtocolFactory.WEB_ORIGINS_SCOPE)); Map optionalClientScopes = realm.getDefaultOptionalClientScopes() .stream().collect(Collectors.toMap(clientScope -> clientScope.getName(), clientScope -> clientScope)); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java index 4017b2d1608..ecd5b636abc 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java @@ -30,6 +30,7 @@ import org.keycloak.models.Constants; import org.keycloak.models.LDAPConstants; import org.keycloak.models.UserModel; import org.keycloak.models.utils.DefaultAuthenticationFlows; +import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory; import org.keycloak.representations.AccessToken; import org.keycloak.representations.idm.AuthenticationExecutionExportRepresentation; import org.keycloak.representations.idm.AuthenticationFlowRepresentation; @@ -58,10 +59,10 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; + import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -69,12 +70,10 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import org.keycloak.common.Profile; import static org.keycloak.models.AccountRoles.MANAGE_ACCOUNT; import static org.keycloak.models.AccountRoles.MANAGE_ACCOUNT_LINKS; import static org.keycloak.models.Constants.ACCOUNT_MANAGEMENT_CLIENT_ID; import static org.keycloak.testsuite.Assert.assertNames; -import org.keycloak.testsuite.ProfileAssume; import static org.keycloak.testsuite.auth.page.AuthRealm.MASTER; /** @@ -227,6 +226,9 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest { if (supportsAuthzService && checkMigrationData) { testGroupPolicyTypeFineGrainedAdminPermission(); } + + // NOTE: Fact that 'roles' and 'web-origins' scope were added was tested in testMigrationTo4_0_0 already + testRolesAndWebOriginsScopesAddedToClient(); } private void testGroupPolicyTypeFineGrainedAdminPermission() { @@ -512,6 +514,24 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest { } + private void testRolesAndWebOriginsScopesAddedToClient() { + log.infof("Testing roles and web-origins default scopes present in realm %s for client migration-test-client", migrationRealm.toRepresentation().getRealm()); + + List defaultClientScopes = ApiUtil.findClientByClientId(this.migrationRealm, "migration-test-client").getDefaultClientScopes(); + + Set defaultClientScopeNames = defaultClientScopes.stream() + .map(ClientScopeRepresentation::getName) + .collect(Collectors.toSet()); + + if (!defaultClientScopeNames.contains(OIDCLoginProtocolFactory.ROLES_SCOPE)) { + Assert.fail("Client scope 'roles' not found as default scope of client migration-test-client"); + } + if (!defaultClientScopeNames.contains(OIDCLoginProtocolFactory.WEB_ORIGINS_SCOPE)) { + Assert.fail("Client scope 'web-origins' not found as default scope of client migration-test-client"); + } + + } + private void testRequiredActionsPriority(RealmResource... realms) { log.info("testing required action's priority"); for (RealmResource realm : realms) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java index e956c2032f3..e644acc66df 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java @@ -99,7 +99,7 @@ public class OAuthGrantTest extends AbstractKeycloakTest { oauth.doLoginGrant("test-user@localhost", "password"); grantPage.assertCurrent(); - grantPage.assertGrants(OAuthGrantPage.PROFILE_CONSENT_TEXT, OAuthGrantPage.EMAIL_CONSENT_TEXT); + grantPage.assertGrants(OAuthGrantPage.PROFILE_CONSENT_TEXT, OAuthGrantPage.EMAIL_CONSENT_TEXT, OAuthGrantPage.ROLES_CONSENT_TEXT); grantPage.accept(); @@ -148,7 +148,7 @@ public class OAuthGrantTest extends AbstractKeycloakTest { oauth.doLoginGrant("test-user@localhost", "password"); grantPage.assertCurrent(); - grantPage.assertGrants(OAuthGrantPage.PROFILE_CONSENT_TEXT, OAuthGrantPage.EMAIL_CONSENT_TEXT); + grantPage.assertGrants(OAuthGrantPage.PROFILE_CONSENT_TEXT, OAuthGrantPage.EMAIL_CONSENT_TEXT, OAuthGrantPage.ROLES_CONSENT_TEXT); grantPage.cancel(); @@ -202,7 +202,7 @@ public class OAuthGrantTest extends AbstractKeycloakTest { // Open login form again and assert grant Page is shown oauth.openLoginForm(); grantPage.assertCurrent(); - grantPage.assertGrants(OAuthGrantPage.PROFILE_CONSENT_TEXT, OAuthGrantPage.EMAIL_CONSENT_TEXT); + grantPage.assertGrants(OAuthGrantPage.PROFILE_CONSENT_TEXT, OAuthGrantPage.EMAIL_CONSENT_TEXT, OAuthGrantPage.ROLES_CONSENT_TEXT); } @Test @@ -346,7 +346,7 @@ public class OAuthGrantTest extends AbstractKeycloakTest { oauth.clientId(THIRD_PARTY_APP); oauth.doLoginGrant("test-user@localhost", "password"); grantPage.assertCurrent(); - grantPage.assertGrants(OAuthGrantPage.EMAIL_CONSENT_TEXT, OAuthGrantPage.PROFILE_CONSENT_TEXT, "foo-addr"); + grantPage.assertGrants(OAuthGrantPage.EMAIL_CONSENT_TEXT, OAuthGrantPage.PROFILE_CONSENT_TEXT, OAuthGrantPage.ROLES_CONSENT_TEXT, "foo-addr"); grantPage.accept(); events.expectLogin() 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 4e19606bf7b..eafcfcdd8c4 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 @@ -22,15 +22,21 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.keycloak.admin.client.resource.ClientResource; +import org.keycloak.admin.client.resource.ClientScopeResource; import org.keycloak.admin.client.resource.ProtocolMappersResource; +import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.admin.client.resource.UserResource; import org.keycloak.common.util.UriUtils; +import org.keycloak.models.AccountRoles; import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory; import org.keycloak.protocol.oidc.mappers.AddressMapper; +import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper; import org.keycloak.representations.AccessToken; import org.keycloak.representations.AddressClaimSet; import org.keycloak.representations.IDToken; import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.ClientScopeRepresentation; import org.keycloak.representations.idm.ProtocolMapperRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.UserRepresentation; @@ -207,11 +213,19 @@ public class OIDCProtocolMappersTest extends AbstractKeycloakTest { assertTrue(departments.contains("finance") && departments.contains("development")); assertTrue(accessToken.getRealmAccess().getRoles().contains("hardcoded")); assertTrue(accessToken.getRealmAccess().getRoles().contains("realm-user")); - Assert.assertFalse(accessToken.getResourceAccess("test-app").getRoles().contains("customer-user")); + Assert.assertNull(accessToken.getResourceAccess("test-app")); assertTrue(accessToken.getResourceAccess("app").getRoles().contains("hardcoded")); assertEquals("hello_test-user@localhost", accessToken.getOtherClaims().get("computed-via-script")); assertEquals(Arrays.asList("A","B"), accessToken.getOtherClaims().get("multiValued-via-script")); + + // Assert audiences added through AudienceResolve mapper + Assert.assertThat(accessToken.getAudience(), arrayContainingInAnyOrder("test-app", "app", "account")); + + // Assert allowed origins + String expectedOrigin = UriUtils.getOrigin(oauth.getRedirectUri()); + Assert.assertNames(accessToken.getAllowedOrigins(), expectedOrigin); + oauth.openLogout(); } @@ -344,6 +358,87 @@ public class OIDCProtocolMappersTest extends AbstractKeycloakTest { deleteMappers(protocolMappers); } + + // Test to update protocolMappers to not have roles on the default position (realm_access and resource_access properties) + @Test + public void testUserRolesMovedFromAccessTokenProperties() throws Exception { + RealmResource realm = adminClient.realm("test"); + ClientScopeResource rolesScope = ApiUtil.findClientScopeByName(realm, OIDCLoginProtocolFactory.ROLES_SCOPE); + + // Update builtin protocolMappers to put roles to different position (claim "custom.roles") for both realm and client roles + ProtocolMapperRepresentation realmRolesMapper = null; + ProtocolMapperRepresentation clientRolesMapper = null; + for (ProtocolMapperRepresentation rep : rolesScope.getProtocolMappers().getMappers()) { + if (OIDCLoginProtocolFactory.REALM_ROLES.equals(rep.getName())) { + realmRolesMapper = rep; + } else if (OIDCLoginProtocolFactory.CLIENT_ROLES.equals(rep.getName())) { + clientRolesMapper = rep; + } + } + + String realmRolesTokenClaimOrig = realmRolesMapper.getConfig().get(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME); + String clientRolesTokenClaimOrig = clientRolesMapper.getConfig().get(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME); + + realmRolesMapper.getConfig().put(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME, "custom.roles"); + rolesScope.getProtocolMappers().update(realmRolesMapper.getId(), realmRolesMapper); + clientRolesMapper.getConfig().put(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME, "custom.roles"); + rolesScope.getProtocolMappers().update(clientRolesMapper.getId(), clientRolesMapper); + + // Create some hardcoded role mapper + Response resp = rolesScope.getProtocolMappers().createMapper(createHardcodedRole("hard-realm", "hardcoded")); + String hardcodedMapperId = ApiUtil.getCreatedId(resp); + resp.close(); + + try { + OAuthClient.AccessTokenResponse response = browserLogin("password", "test-user@localhost", "password"); + AccessToken accessToken = oauth.verifyToken(response.getAccessToken()); + + // Assert roles are not on their original positions + Assert.assertNull(accessToken.getRealmAccess()); + Assert.assertTrue(accessToken.getResourceAccess().isEmpty()); + + // Assert both realm and client roles on the new position. Hardcoded role should be here as well + Map cst1 = (Map) accessToken.getOtherClaims().get("custom"); + List roles = (List) cst1.get("roles"); + Assert.assertNames(roles, "offline_access", "user", "customer-user", "hardcoded", AccountRoles.VIEW_PROFILE, AccountRoles.MANAGE_ACCOUNT, AccountRoles.MANAGE_ACCOUNT_LINKS); + + // Assert audience + Assert.assertNames(Arrays.asList(accessToken.getAudience()), "account", "test-app"); + } finally { + // Revert + rolesScope.getProtocolMappers().delete(hardcodedMapperId); + + realmRolesMapper.getConfig().put(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME, realmRolesTokenClaimOrig); + rolesScope.getProtocolMappers().update(realmRolesMapper.getId(), realmRolesMapper); + clientRolesMapper.getConfig().put(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME, clientRolesTokenClaimOrig); + rolesScope.getProtocolMappers().update(clientRolesMapper.getId(), clientRolesMapper); + } + } + + + @Test + public void testAllowedOriginsRemovedFromAccessToken() throws Exception { + RealmResource realm = adminClient.realm("test"); + ClientScopeRepresentation allowedOriginsScope = ApiUtil.findClientScopeByName(realm, OIDCLoginProtocolFactory.WEB_ORIGINS_SCOPE).toRepresentation(); + + // Remove 'web-origins' scope from the client + ClientResource testApp = ApiUtil.findClientByClientId(realm, "test-app"); + testApp.removeDefaultClientScope(allowedOriginsScope.getId()); + + try { + OAuthClient.AccessTokenResponse response = browserLogin("password", "test-user@localhost", "password"); + AccessToken accessToken = oauth.verifyToken(response.getAccessToken()); + + // Assert web origins are not in the token + Assert.assertNull(accessToken.getAllowedOrigins()); + + } finally { + // Revert + testApp.addDefaultClientScope(allowedOriginsScope.getId()); + } + } + + /** * KEYCLOAK-4205 * @throws Exception @@ -381,6 +476,55 @@ public class OIDCProtocolMappersTest extends AbstractKeycloakTest { deleteMappers(protocolMappers); } + + /** + * KEYCLOAK-5259 + * @throws Exception + */ + @Test + public void testUserRoleToAttributeMappersWithFullScopeDisabled() throws Exception { + // Add mapper for realm roles + ProtocolMapperRepresentation realmMapper = ProtocolMapperUtil.createUserRealmRoleMappingMapper("pref.", "Realm roles mapper", "roles-custom.realm", true, true, true); + ProtocolMapperRepresentation clientMapper = ProtocolMapperUtil.createUserClientRoleMappingMapper("test-app", null, "Client roles mapper", "roles-custom.test-app", true, true, true); + + ClientResource client = ApiUtil.findClientResourceByClientId(adminClient.realm("test"), "test-app"); + + // Disable full-scope-allowed + ClientRepresentation rep = client.toRepresentation(); + rep.setFullScopeAllowed(false); + client.update(rep); + + ProtocolMappersResource protocolMappers = ApiUtil.findClientResourceByClientId(adminClient.realm("test"), "test-app").getProtocolMappers(); + protocolMappers.createMapper(Arrays.asList(realmMapper, clientMapper)); + + // Login user + OAuthClient.AccessTokenResponse response = browserLogin("password", "test-user@localhost", "password"); + IDToken idToken = oauth.verifyIDToken(response.getIdToken()); + + // Verify attribute is filled + Map roleMappings = (Map)idToken.getOtherClaims().get("roles-custom"); + Assert.assertThat(roleMappings.keySet(), containsInAnyOrder("realm", "test-app")); + Assert.assertThat(roleMappings.get("realm"), CoreMatchers.instanceOf(List.class)); + Assert.assertThat(roleMappings.get("test-app"), CoreMatchers.instanceOf(List.class)); + + List realmRoleMappings = (List) roleMappings.get("realm"); + List testAppMappings = (List) roleMappings.get("test-app"); + assertRoles(realmRoleMappings, + "pref.user" // from direct assignment in user definition + ); + assertRoles(testAppMappings, + "customer-user" // from direct assignment in user definition + ); + + // Revert + deleteMappers(protocolMappers); + + rep = client.toRepresentation(); + rep.setFullScopeAllowed(true); + client.update(rep); + } + + @Test public void testUserGroupRoleToAttributeMappers() throws Exception { // Add mapper for realm roles @@ -442,7 +586,7 @@ public class OIDCProtocolMappersTest extends AbstractKeycloakTest { // Verify attribute is filled Map roleMappings = (Map)idToken.getOtherClaims().get("roles-custom"); - Assert.assertThat(roleMappings.keySet(), containsInAnyOrder("realm", clientId)); + Assert.assertThat(roleMappings.keySet(), containsInAnyOrder("realm")); String realmRoleMappings = (String) roleMappings.get("realm"); String testAppAuthzMappings = (String) roleMappings.get(clientId); assertRolesString(realmRoleMappings, @@ -452,7 +596,7 @@ public class OIDCProtocolMappersTest extends AbstractKeycloakTest { "pref.realm-composite-role", // from parent group of /roleRichGroup/level2group, i.e. from /roleRichGroup "pref.sample-realm-role" // from realm role realm-composite-role ); - assertRolesString(testAppAuthzMappings); // There is no client role defined for test-app-authz + assertNull(testAppAuthzMappings); // There is no client role defined for test-app-authz // Revert deleteMappers(protocolMappers); @@ -480,10 +624,12 @@ public class OIDCProtocolMappersTest extends AbstractKeycloakTest { String testAppScopeMappings = (String) roleMappings.get(clientId); assertRolesString(realmRoleMappings, "pref.admin", // from direct assignment to /roleRichGroup/level2group - "pref.user" // from parent group of /roleRichGroup/level2group, i.e. from /roleRichGroup + "pref.user", // from parent group of /roleRichGroup/level2group, i.e. from /roleRichGroup + "pref.customer-user-premium" ); assertRolesString(testAppScopeMappings, - "test-app-allowed-by-scope" // from direct assignment to roleRichUser, present as scope allows it + "test-app-allowed-by-scope", // from direct assignment to roleRichUser, present as scope allows it + "test-app-disallowed-by-scope" ); // Revert @@ -512,11 +658,12 @@ public class OIDCProtocolMappersTest extends AbstractKeycloakTest { String testAppScopeMappings = (String) roleMappings.get(clientId); assertRolesString(realmRoleMappings, "pref.admin", // from direct assignment to /roleRichGroup/level2group - "pref.user" // from parent group of /roleRichGroup/level2group, i.e. from /roleRichGroup + "pref.user", // from parent group of /roleRichGroup/level2group, i.e. from /roleRichGroup + "pref.customer-user-premium" ); assertRolesString(testAppScopeMappings, "test-app-allowed-by-scope", // from direct assignment to roleRichUser, present as scope allows it - "customer-admin-composite-role" // from direct assignment to /roleRichGroup/level2group, present as scope allows it + "test-app-disallowed-by-scope" // from direct assignment to /roleRichGroup/level2group, present as scope allows it ); // Revert diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java index 2c7796252bc..ee3b3b47ded 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java @@ -18,6 +18,7 @@ package org.keycloak.testsuite.oauth; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; import org.junit.Rule; import org.junit.Test; import org.keycloak.OAuth2Constants; @@ -118,7 +119,11 @@ public class TokenIntrospectionTest extends AbstractTestRealmKeycloakTest { assertEquals(jsonNode.get("iat").asInt(), rep.getIssuedAt()); assertEquals(jsonNode.get("nbf").asInt(), rep.getNotBefore()); assertEquals(jsonNode.get("sub").asText(), rep.getSubject()); - assertEquals(jsonNode.get("aud").asText(), rep.getAudience()[0]); + + List audiences = new ArrayList<>(); + jsonNode.get("aud").forEach(childNode -> audiences.add(childNode.asText())); + Assert.assertNames(audiences, rep.getAudience()); + assertEquals(jsonNode.get("iss").asText(), rep.getIssuer()); assertEquals(jsonNode.get("jti").asText(), rep.getId()); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/AudienceTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/AudienceTest.java index ba9186b7dfd..a810f5da375 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/AudienceTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/AudienceTest.java @@ -24,6 +24,7 @@ import javax.ws.rs.core.Response; import org.jboss.arquillian.container.test.api.Deployment; import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Before; import org.junit.Test; import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.admin.client.resource.ClientScopeResource; @@ -74,16 +75,10 @@ public class AudienceTest extends AbstractOIDCScopeTest { role1.setName("role1"); testRealm.getRoles().getClient().put("service-client", Arrays.asList(role1)); - // Create client scope 'audience-scope' and add as optional scope to the 'test-app' client - ClientScopeRepresentation clientScope = new ClientScopeRepresentation(); - clientScope.setName("audience-scope"); - clientScope.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); - testRealm.setClientScopes(Arrays.asList(clientScope)); - + // Disable FullScopeAllowed for the 'test-app' client ClientRepresentation testApp = testRealm.getClients().stream().filter((ClientRepresentation client) -> { return "test-app".equals(client.getClientId()); }).findFirst().get(); - testApp.setOptionalClientScopes(Arrays.asList("audience-scope")); testApp.setFullScopeAllowed(false); @@ -103,6 +98,26 @@ public class AudienceTest extends AbstractOIDCScopeTest { testRealm.getUsers().add(user); } + @Before + public void beforeTest() { + // Check if already exists + ClientScopeResource clientScopeRes = ApiUtil.findClientScopeByName(testRealm(), "audience-scope"); + if (clientScopeRes != null) { + return; + } + + // Create client scope 'audience-scope' and add as optional scope to the 'test-app' client + ClientScopeRepresentation clientScope = new ClientScopeRepresentation(); + clientScope.setName("audience-scope"); + clientScope.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + Response resp = testRealm().clientScopes().create(clientScope); + String clientScopeId = ApiUtil.getCreatedId(resp); + resp.close(); + + ClientResource client = ApiUtil.findClientByClientId(testRealm(), "test-app"); + client.addOptionalClientScope(clientScopeId); + } + @Test public void testAudienceProtocolMapperWithClientAudience() throws Exception { @@ -120,7 +135,7 @@ public class AudienceTest extends AbstractOIDCScopeTest { EventRepresentation loginEvent = events.expectLogin() .user(userId) .assertEvent(); - Tokens tokens = sendTokenRequest(loginEvent, userId,"openid audience-scope", "test-app"); + Tokens tokens = sendTokenRequest(loginEvent, userId,"openid profile email audience-scope", "test-app"); // TODO: Frontend client itself should not be in the audiences of access token. Will be fixed in the future assertAudiences(tokens.accessToken, "test-app", "service-client"); assertAudiences(tokens.idToken, "test-app"); @@ -152,7 +167,7 @@ public class AudienceTest extends AbstractOIDCScopeTest { EventRepresentation loginEvent = events.expectLogin() .user(userId) .assertEvent(); - Tokens tokens = sendTokenRequest(loginEvent, userId,"openid audience-scope", "test-app"); + Tokens tokens = sendTokenRequest(loginEvent, userId,"openid profile email audience-scope", "test-app"); // TODO: Frontend client itself should not be in the audiences of access token. Will be fixed in the future assertAudiences(tokens.accessToken, "test-app", "http://host/service/ctx1", "http://host/service/ctx2"); assertAudiences(tokens.idToken, "test-app", "http://host/service/ctx2"); @@ -176,7 +191,7 @@ public class AudienceTest extends AbstractOIDCScopeTest { EventRepresentation loginEvent = events.expectLogin() .user(userId) .assertEvent(); - Tokens tokens = sendTokenRequest(loginEvent, userId,"openid", "test-app"); + Tokens tokens = sendTokenRequest(loginEvent, userId,"openid profile email", "test-app"); assertAudiences(tokens.accessToken, "test-app"); assertAudiences(tokens.idToken, "test-app"); Assert.assertFalse(tokens.accessToken.getResourceAccess().containsKey("service-client")); @@ -199,7 +214,7 @@ public class AudienceTest extends AbstractOIDCScopeTest { loginEvent = events.expectLogin() .user(userId) .assertEvent(); - tokens = sendTokenRequest(loginEvent, userId,"openid service-client", "test-app"); + tokens = sendTokenRequest(loginEvent, userId,"openid profile email service-client", "test-app"); assertAudiences(tokens.accessToken, "test-app", "service-client"); assertAudiences(tokens.idToken, "test-app"); Assert.assertTrue(tokens.accessToken.getResourceAccess().containsKey("service-client")); 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 b4a64b8625b..1c1a590b0ec 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 @@ -274,7 +274,7 @@ public class OIDCScopeTest extends AbstractOIDCScopeTest { oauth.doLoginGrant("john", "password"); grantPage.assertCurrent(); - grantPage.assertGrants(OAuthGrantPage.PROFILE_CONSENT_TEXT, OAuthGrantPage.EMAIL_CONSENT_TEXT); + grantPage.assertGrants(OAuthGrantPage.PROFILE_CONSENT_TEXT, OAuthGrantPage.EMAIL_CONSENT_TEXT, OAuthGrantPage.ROLES_CONSENT_TEXT); grantPage.accept(); EventRepresentation loginEvent = events.expectLogin() @@ -339,7 +339,7 @@ public class OIDCScopeTest extends AbstractOIDCScopeTest { oauth.doLoginGrant("john", "password"); grantPage.assertCurrent(); - grantPage.assertGrants(OAuthGrantPage.PROFILE_CONSENT_TEXT, OAuthGrantPage.EMAIL_CONSENT_TEXT, "ThirdParty permissions"); + grantPage.assertGrants(OAuthGrantPage.PROFILE_CONSENT_TEXT, OAuthGrantPage.EMAIL_CONSENT_TEXT, OAuthGrantPage.ROLES_CONSENT_TEXT, "ThirdParty permissions"); grantPage.accept(); EventRepresentation loginEvent = events.expectLogin() @@ -369,7 +369,7 @@ public class OIDCScopeTest extends AbstractOIDCScopeTest { oauth.doLoginGrant("john", "password"); grantPage.assertCurrent(); - grantPage.assertGrants(OAuthGrantPage.PROFILE_CONSENT_TEXT, OAuthGrantPage.EMAIL_CONSENT_TEXT); + grantPage.assertGrants(OAuthGrantPage.PROFILE_CONSENT_TEXT, OAuthGrantPage.EMAIL_CONSENT_TEXT, OAuthGrantPage.ROLES_CONSENT_TEXT); grantPage.accept(); EventRepresentation loginEvent = events.expectLogin() diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java index c8084e0a992..26a740e5289 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java @@ -27,6 +27,7 @@ import org.keycloak.OAuth2Constants; import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.crypto.Algorithm; import org.keycloak.jose.jwk.JSONWebKeySet; +import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory; import org.keycloak.protocol.oidc.OIDCLoginProtocolService; import org.keycloak.protocol.oidc.OIDCWellKnownProviderFactory; import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation; @@ -139,7 +140,8 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest { // Scopes supported Assert.assertNames(oidcConfig.getScopesSupported(), OAuth2Constants.SCOPE_OPENID, OAuth2Constants.OFFLINE_ACCESS, - OAuth2Constants.SCOPE_PROFILE, OAuth2Constants.SCOPE_EMAIL, OAuth2Constants.SCOPE_PHONE, OAuth2Constants.SCOPE_ADDRESS); + OAuth2Constants.SCOPE_PROFILE, OAuth2Constants.SCOPE_EMAIL, OAuth2Constants.SCOPE_PHONE, OAuth2Constants.SCOPE_ADDRESS, + OIDCLoginProtocolFactory.ROLES_SCOPE, OIDCLoginProtocolFactory.WEB_ORIGINS_SCOPE); // Request and Request_Uri Assert.assertTrue(oidcConfig.getRequestParameterSupported()); diff --git a/testsuite/integration-deprecated/src/test/resources/broker-test/realm-with-oidc-property-mappers.json b/testsuite/integration-deprecated/src/test/resources/broker-test/realm-with-oidc-property-mappers.json index 2ed192c197b..dbe824ddbe1 100755 --- a/testsuite/integration-deprecated/src/test/resources/broker-test/realm-with-oidc-property-mappers.json +++ b/testsuite/integration-deprecated/src/test/resources/broker-test/realm-with-oidc-property-mappers.json @@ -13,7 +13,7 @@ "enabled": true, "fullScopeAllowed": true, "secret": "secret", - "defaultClientScopes": [], + "defaultClientScopes": [ "roles", "web-origins" ], "redirectUris": [ "http://localhost:8081/auth/realms/realm-with-broker/broker/kc-oidc-idp-property-mappers/endpoint/*" ], diff --git a/themes/src/main/resources/theme/base/account/messages/messages_en.properties b/themes/src/main/resources/theme/base/account/messages/messages_en.properties index d2acee2a650..275c248442a 100755 --- a/themes/src/main/resources/theme/base/account/messages/messages_en.properties +++ b/themes/src/main/resources/theme/base/account/messages/messages_en.properties @@ -60,6 +60,7 @@ addressScopeConsentText=Address phoneScopeConsentText=Phone number offlineAccessScopeConsentText=Offline Access samlRoleListScopeConsentText=My Roles +rolesScopeConsentText=User roles role_admin=Admin role_realm-admin=Realm Admin diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties index 17c5a62e3e2..78766300464 100644 --- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties +++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties @@ -219,9 +219,10 @@ includeInAccessToken.tooltip=Should the claim be added to the access token? includeInUserInfo.label=Add to userinfo includeInUserInfo.tooltip=Should the claim be added to the userinfo? usermodel.clientRoleMapping.clientId.label=Client ID -usermodel.clientRoleMapping.clientId.tooltip=Client ID for role mappings +usermodel.clientRoleMapping.clientId.tooltip=Client ID for role mappings. Just client roles of this client will be added to the token. If this is unset, then client roles of all clients will be added to the token. usermodel.clientRoleMapping.rolePrefix.label=Client Role prefix usermodel.clientRoleMapping.rolePrefix.tooltip=A prefix for each client role (optional). +usermodel.clientRoleMapping.tokenClaimName.tooltip=Name of the claim to insert into the token. This can be a fully qualified name like 'address.street'. In this case, a nested json object will be created. To prevent nesting and use dot literally, escape the dot with backslash (\\.). The special token ${client_id} can be used and this will be replaced by the actual client ID. Example usage is 'resource_access.${client_id}.roles'. This is useful especialy when you are adding roles from all the clients (Hence 'Client ID' switch is unset) and you want client roles of each client in separate place. usermodel.realmRoleMapping.rolePrefix.label=Realm Role prefix usermodel.realmRoleMapping.rolePrefix.tooltip=A prefix for each Realm Role (optional). sectorIdentifierUri.label=Sector Identifier URI @@ -432,6 +433,7 @@ client.associated-roles.tooltip=Client roles associated with this composite role add-builtin=Add Builtin category=Category type=Type +priority-order=Priority Order no-mappers-available=No mappers available add-builtin-protocol-mappers=Add Builtin Protocol Mappers add-builtin-protocol-mapper=Add Builtin Protocol Mapper @@ -850,6 +852,8 @@ client-scope.consent-screen-text=Consent Screen Text client-scope.consent-screen-text.tooltip=Text, which will be shown on consent screen when this client scope is added to some client with consent required. Defaults to name of client scope if it's not filled client-scope.gui-order=GUI order client-scope.gui-order.tooltip=Specify order of the provider in GUI (e.g. in Consent page) as integer +client-scope.include-in-token-scope=Include In Token Scope +client-scope.include-in-token-scope.tooltip=If on, then the name of this client scope will be added to the access token property 'scope' as well as to the Token Introspection Endpoint response. If off, then this client scope will be ommitted from the token and from the Token Introspection Endpoint response. add-user-federation-provider=Add user federation provider add-user-storage-provider=Add user storage provider diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js index b8ffc66bea3..a1c1172ef0e 100755 --- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js @@ -1898,6 +1898,10 @@ module.controller('ClientProtocolMapperListCtrl', function($scope, realm, client }); }; + $scope.sortMappersByPriority = function(mapper) { + return $scope.mapperTypes[mapper.protocolMapper].priority; + } + var updateMappers = function() { $scope.mappers = ClientProtocolMappersByProtocol.query({realm : realm.realm, client : client.id, protocol : client.protocol}); }; @@ -2391,6 +2395,10 @@ module.controller('ClientClientScopesEvaluateCtrl', function($scope, Realm, User return $scope.selectedTab === 3; } + $scope.sortMappersByPriority = function(mapper) { + return $scope.mapperTypes[mapper.protocolMapper].priority; + } + // Roles @@ -2693,6 +2701,16 @@ module.controller('ClientScopeDetailCtrl', function($scope, realm, clientScope, } else { $scope.displayOnConsentScreen = true; } + + if ($scope.clientScope.attributes["include.in.token.scope"]) { + if ($scope.clientScope.attributes["include.in.token.scope"] == "true") { + $scope.includeInTokenScope = true; + } else { + $scope.includeInTokenScope = false; + } + } else { + $scope.includeInTokenScope = true; + } } if (!$scope.create) { @@ -2742,6 +2760,12 @@ module.controller('ClientScopeDetailCtrl', function($scope, realm, clientScope, $scope.clientScope.attributes["display.on.consent.screen"] = "false"; } + if ($scope.includeInTokenScope == true) { + $scope.clientScope.attributes["include.in.token.scope"] = "true"; + } else { + $scope.clientScope.attributes["include.in.token.scope"] = "false"; + } + if ($scope.create) { ClientScope.save({ realm: realm.realm, @@ -2801,6 +2825,10 @@ module.controller('ClientScopeProtocolMapperListCtrl', function($scope, realm, c }); }; + $scope.sortMappersByPriority = function(mapper) { + return $scope.mapperTypes[mapper.protocolMapper].priority; + } + var updateMappers = function() { $scope.mappers = ClientScopeProtocolMappersByProtocol.query({realm : realm.realm, clientScope : clientScope.id, protocol : clientScope.protocol}); }; diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/client-mappers.html b/themes/src/main/resources/theme/base/admin/resources/partials/client-mappers.html index f600b08f76a..83123c27804 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/client-mappers.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/client-mappers.html @@ -32,14 +32,16 @@ {{:: 'name' | translate}} {{:: 'category' | translate}} {{:: 'type' | translate}} + {{:: 'priority-order' | translate}} {{:: 'actions' | translate}} - + {{mapper.name}} {{mapperTypes[mapper.protocolMapper].category}} {{mapperTypes[mapper.protocolMapper].name}} + {{mapperTypes[mapper.protocolMapper].priority}} {{:: 'edit' | translate}} {{:: 'delete' | translate}} diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/client-scope-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/client-scope-detail.html index 8daee59e40f..22999937931 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/client-scope-detail.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/client-scope-detail.html @@ -53,6 +53,13 @@ {{:: 'client-scope.consent-screen-text.tooltip' | translate}} +
+ +
+ +
+ {{:: 'client-scope.include-in-token-scope.tooltip' | translate}} +
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/client-scope-mappers.html b/themes/src/main/resources/theme/base/admin/resources/partials/client-scope-mappers.html index 661be83bf33..3b95910913b 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/client-scope-mappers.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/client-scope-mappers.html @@ -32,14 +32,16 @@ {{:: 'name' | translate}} {{:: 'category' | translate}} {{:: 'type' | translate}} + {{:: 'priority-order' | translate}} {{:: 'actions' | translate}} - + {{mapper.name}} {{mapperTypes[mapper.protocolMapper].category}} {{mapperTypes[mapper.protocolMapper].name}} + {{mapperTypes[mapper.protocolMapper].priority}} {{:: 'edit' | translate}} {{:: 'delete' | translate}} diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/client-scopes-evaluate.html b/themes/src/main/resources/theme/base/admin/resources/partials/client-scopes-evaluate.html index 34b8971ac4a..8b221b8f18b 100644 --- a/themes/src/main/resources/theme/base/admin/resources/partials/client-scopes-evaluate.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/client-scopes-evaluate.html @@ -145,14 +145,16 @@ {{:: 'parent-client-scope' | translate}} {{:: 'category' | translate}} {{:: 'type' | translate}} + {{:: 'priority-order' | translate}} - + {{mapper.mapperName}} {{mapper.containerName}} {{mapperTypes[mapper.protocolMapper].category}} {{mapperTypes[mapper.protocolMapper].name}} + {{mapperTypes[mapper.protocolMapper].priority}} {{:: 'no-mappers-available' | translate}} diff --git a/themes/src/main/resources/theme/base/login/messages/messages_en.properties b/themes/src/main/resources/theme/base/login/messages/messages_en.properties index 2ca0d708a45..c1472b72413 100755 --- a/themes/src/main/resources/theme/base/login/messages/messages_en.properties +++ b/themes/src/main/resources/theme/base/login/messages/messages_en.properties @@ -79,6 +79,7 @@ addressScopeConsentText=Address phoneScopeConsentText=Phone number offlineAccessScopeConsentText=Offline Access samlRoleListScopeConsentText=My Roles +rolesScopeConsentText=User roles loginTotpIntro=You are required to set up a One Time Password generator to access this account loginTotpStep1=Install one of the following applications on your mobile