KEYCLOAK-6884 KEYCLOAK-3454 KEYCLOAK-8298 Default 'roles' and 'web-origins' client scopes. Add roles and allowed-origins to the token through protocol mappers
This commit is contained in:
parent
dba513c921
commit
2a4cee6044
52 changed files with 1163 additions and 262 deletions
|
@ -28,6 +28,7 @@ public class ProtocolMapperTypeRepresentation {
|
|||
protected String name;
|
||||
protected String category;
|
||||
protected String helpText;
|
||||
protected int priority;
|
||||
|
||||
protected List<ConfigPropertyRepresentation> 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<ConfigPropertyRepresentation> getProperties() {
|
||||
return properties;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
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());
|
||||
}
|
||||
}
|
|
@ -34,6 +34,14 @@ public interface ProtocolMapper extends Provider, ProviderFactory<ProtocolMapper
|
|||
String getDisplayCategory();
|
||||
String getDisplayType();
|
||||
|
||||
/**
|
||||
* Priority of this protocolMapper implementation. Lower goes first.
|
||||
* @return
|
||||
*/
|
||||
default int getPriority() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when instance of mapperModel is created/updated for this protocolMapper through admin endpoint
|
||||
*
|
||||
|
|
|
@ -49,6 +49,7 @@ public interface ClientScopeModel extends ProtocolMapperContainerModel, ScopeCon
|
|||
String DISPLAY_ON_CONSENT_SCREEN = "display.on.consent.screen";
|
||||
String CONSENT_SCREEN_TEXT = "consent.screen.text";
|
||||
String GUI_ORDER = "gui.order";
|
||||
String INCLUDE_IN_TOKEN_SCOPE = "include.in.token.scope";
|
||||
|
||||
default boolean isDisplayOnConsentScreen() {
|
||||
String displayVal = getAttribute(DISPLAY_ON_CONSENT_SCREEN);
|
||||
|
@ -81,5 +82,14 @@ public interface ClientScopeModel extends ProtocolMapperContainerModel, ScopeCon
|
|||
setAttribute(GUI_ORDER, guiOrder);
|
||||
}
|
||||
|
||||
default boolean isIncludeInTokenScope() {
|
||||
String includeInTokenScope = getAttribute(INCLUDE_IN_TOKEN_SCOPE);
|
||||
return includeInTokenScope==null ? true : Boolean.parseBoolean(includeInTokenScope);
|
||||
}
|
||||
|
||||
default void setIncludeInTokenScope(boolean includeInTokenScope) {
|
||||
setAttribute(INCLUDE_IN_TOKEN_SCOPE, String.valueOf(includeInTokenScope));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -17,7 +17,9 @@
|
|||
|
||||
package org.keycloak.protocol;
|
||||
|
||||
import org.keycloak.models.ClientSessionContext;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.ProtocolMapperModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
|
@ -25,6 +27,12 @@ import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
|
|||
import org.keycloak.provider.ProviderFactory;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
|
@ -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<Map.Entry<ProtocolMapperModel, ProtocolMapper>> getSortedProtocolMappers(KeycloakSession session, ClientSessionContext ctx) {
|
||||
Set<ProtocolMapperModel> mapperModels = ctx.getProtocolMappers();
|
||||
Map<ProtocolMapperModel, ProtocolMapper> 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<ProtocolMapperModel, ProtocolMapper> entry) {
|
||||
int priority = entry.getValue().getPriority();
|
||||
return priority;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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<ProtocolMapperModel> mappings = clientSessionCtx.getProtocolMappers();
|
||||
for (final ProtocolMapperModel mapping : mappings) {
|
||||
final ProtocolMapper mapper = (ProtocolMapper) session.getKeycloakSessionFactory().getProviderFactory(ProtocolMapper.class, mapping.getProtocolMapper());
|
||||
|
||||
for (Map.Entry<ProtocolMapperModel, ProtocolMapper> 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)) {
|
||||
|
|
|
@ -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) {
|
||||
}
|
||||
|
|
|
@ -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<RoleModel> 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<ProtocolMapperModel> mappings = clientSessionCtx.getProtocolMappers();
|
||||
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
|
||||
for (ProtocolMapperModel mapping : mappings) {
|
||||
|
||||
ProtocolMapper mapper = (ProtocolMapper)sessionFactory.getProviderFactory(ProtocolMapper.class, mapping.getProtocolMapper());
|
||||
for (Map.Entry<ProtocolMapperModel, ProtocolMapper> 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<ProtocolMapperModel> mappings = clientSessionCtx.getProtocolMappers();
|
||||
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
|
||||
for (ProtocolMapperModel mapping : mappings) {
|
||||
|
||||
ProtocolMapper mapper = (ProtocolMapper)sessionFactory.getProviderFactory(ProtocolMapper.class, mapping.getProtocolMapper());
|
||||
for (Map.Entry<ProtocolMapperModel, ProtocolMapper> 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<ProtocolMapperModel> mappings = clientSessionCtx.getProtocolMappers();
|
||||
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
|
||||
for (ProtocolMapperModel mapping : mappings) {
|
||||
|
||||
ProtocolMapper mapper = (ProtocolMapper)sessionFactory.getProviderFactory(ProtocolMapper.class, mapping.getProtocolMapper());
|
||||
for (Map.Entry<ProtocolMapperModel, ProtocolMapper> 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<String> 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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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:
|
||||
* <ul>
|
||||
* <li>Direct assignment of the role to the user</li>
|
||||
* <li>Direct assignment of the role to any group of the user or any of its parent group</li>
|
||||
* <li>Composite roles are expanded recursively, the composite role itself is also contained in the returned stream</li>
|
||||
* </ul>
|
||||
* @param user User to enumerate the roles for
|
||||
* @return
|
||||
*/
|
||||
public static Stream<RoleModel> 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<GroupModel> groupAndItsParentsStream(GroupModel group) {
|
||||
Stream.Builder<GroupModel> 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<RoleModel> restriction, String prefix) {
|
||||
String rolePrefix = prefix == null ? "" : prefix;
|
||||
UserModel user = userSession.getUser();
|
||||
protected static void setClaim(IDToken token, ProtocolMapperModel mappingModel, Set<String> rolesToAdd,
|
||||
String clientId, String prefix) {
|
||||
|
||||
// get a set of all realm roles assigned to the user or its group
|
||||
Stream<RoleModel> clientUserRoles = getAllUserRolesStream(user).filter(restriction);
|
||||
|
||||
boolean dontLimitScope = userSession.getAuthenticatedClientSessions().values().stream().anyMatch(cs -> cs.getClient().isFullScopeAllowed());
|
||||
if (! dontLimitScope) {
|
||||
Set<RoleModel> clientRoles = userSession.getAuthenticatedClientSessions().values().stream()
|
||||
.flatMap(cs -> cs.getClient().getScopeMappings().stream())
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
clientUserRoles = clientUserRoles.filter(clientRoles::contains);
|
||||
Set<String> realmRoleNames;
|
||||
if (prefix != null && !prefix.isEmpty()) {
|
||||
realmRoleNames = rolesToAdd.stream()
|
||||
.map(roleName -> prefix + roleName)
|
||||
.collect(Collectors.toSet());
|
||||
} else {
|
||||
realmRoleNames = rolesToAdd;
|
||||
}
|
||||
|
||||
List<String> 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<String> split = OIDCAttributeMapperHelper.splitClaimPath(protocolClaim);
|
||||
|
||||
// Special case
|
||||
if (checkAccessToken(token, split, attributeValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int length = split.size();
|
||||
int i = 0;
|
||||
Map<String, Object> 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<String, Object> nested = (Map<String, Object>)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<String> path, Object attributeValue) {
|
||||
if (!(idToken instanceof AccessToken)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(attributeValue instanceof Collection)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Collection<String> roles = (Collection<String>) 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class AllowedWebOriginsProtocolMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper {
|
||||
|
||||
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
|
||||
|
||||
|
||||
public static final String PROVIDER_ID = "oidc-allowed-origins-mapper";
|
||||
|
||||
|
||||
public List<ProviderConfigProperty> 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<String> 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class AudienceResolveProtocolMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper {
|
||||
|
||||
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
|
||||
|
||||
|
||||
public static final String PROVIDER_ID = "oidc-audience-resolve-mapper";
|
||||
|
||||
|
||||
public List<ProviderConfigProperty> 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<String, AccessToken.Access> 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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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<Object> valueAsList = (List<Object>) attributeValue;
|
||||
if (attributeValue instanceof Collection) {
|
||||
Collection<Object> valueAsList = (Collection<Object>) 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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<ProviderConfigProperty> 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<String, AccessToken.Access> allAccess = RoleResolveUtil.getAllResolvedClientRoles(session, clientSessionCtx);
|
||||
|
||||
for (Map.Entry<String, AccessToken.Access> 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<RoleModel> 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<RoleModel> clientRoleMappings = client.getRoles();
|
||||
if (fullScopeAllowed) {
|
||||
return clientRoleMappings::contains;
|
||||
}
|
||||
|
||||
Set<RoleModel> 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<ClientScopeModel> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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<ProtocolMapperProcessor<SAMLLoginResponseMapper>> loginResponseMappers = new LinkedList<>();
|
||||
ProtocolMapperProcessor<SAMLRoleListMapper> roleListMapper = null;
|
||||
|
||||
Set<ProtocolMapperModel> mappings = clientSessionCtx.getProtocolMappers();
|
||||
for (ProtocolMapperModel mapping : mappings) {
|
||||
for (Map.Entry<ProtocolMapperModel, ProtocolMapper> 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>((SAMLAttributeStatementMapper) mapper, mapping));
|
||||
}
|
||||
|
|
|
@ -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<SamlProtocol.ProtocolMapperProcessor<SAMLRoleNameMapper>> roleNameMappers = new LinkedList<>();
|
||||
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
|
||||
AttributeType singleAttributeType = null;
|
||||
Set<ProtocolMapperModel> requestedProtocolMappers = clientSessionCtx.getProtocolMappers();
|
||||
for (ProtocolMapperModel mapping : requestedProtocolMappers) {
|
||||
|
||||
ProtocolMapper mapper = (ProtocolMapper)sessionFactory.getProviderFactory(ProtocolMapper.class, mapping.getProtocolMapper());
|
||||
if (mapper == null) continue;
|
||||
for (Map.Entry<ProtocolMapperModel, ProtocolMapper> 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));
|
||||
|
|
|
@ -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() {
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<ConfigPropertyRepresentation>());
|
||||
List<ProviderConfigProperty> configProperties = mapper.getConfigProperties();
|
||||
rep.setProperties(ModelToRepresentation.toRepresentation(configProperties));
|
||||
|
|
|
@ -141,6 +141,10 @@ public class DefaultClientSessionContext implements ClientSessionContext {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (!clientScope.isIncludeInTokenScope()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
|
|
150
services/src/main/java/org/keycloak/utils/RoleResolveUtil.java
Normal file
150
services/src/main/java/org/keycloak/utils/RoleResolveUtil.java
Normal file
|
@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
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<String, AccessToken.Access> 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<RoleModel> 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
|
@ -219,7 +222,12 @@ public class HardcodedClientStorageProvider implements ClientStorageProvider, Cl
|
|||
@Override
|
||||
public Map<String, ClientScopeModel> 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);
|
||||
|
|
|
@ -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<String> 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<String> expected = Arrays.asList(expectedGrants);
|
||||
Assert.assertTrue("Not matched grants. Displayed grants: " + displayed + ", expected grants: " + expected,
|
||||
displayed.containsAll(expected) && expected.containsAll(displayed));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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<String, ClientScopeRepresentation> optionalClientScopes = realm.getDefaultOptionalClientScopes()
|
||||
.stream().collect(Collectors.toMap(clientScope -> clientScope.getName(), clientScope -> clientScope));
|
||||
|
|
|
@ -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<ClientScopeRepresentation> defaultClientScopes = ApiUtil.findClientByClientId(this.migrationRealm, "migration-test-client").getDefaultClientScopes();
|
||||
|
||||
Set<String> 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) {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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<String, Object> cst1 = (Map<String, Object>) accessToken.getOtherClaims().get("custom");
|
||||
List<String> roles = (List<String>) 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<String, Object> roleMappings = (Map<String, Object>)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<String> realmRoleMappings = (List<String>) roleMappings.get("realm");
|
||||
List<String> testAppMappings = (List<String>) 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<String, Object> roleMappings = (Map<String, Object>)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
|
||||
|
|
|
@ -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<String> 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());
|
||||
}
|
||||
|
|
|
@ -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"));
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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/*"
|
||||
],
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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});
|
||||
};
|
||||
|
|
|
@ -32,14 +32,16 @@
|
|||
<th>{{:: 'name' | translate}}</th>
|
||||
<th>{{:: 'category' | translate}}</th>
|
||||
<th>{{:: 'type' | translate}}</th>
|
||||
<th>{{:: 'priority-order' | translate}}</th>
|
||||
<th colspan="2">{{:: 'actions' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="mapper in mappers | filter:search">
|
||||
<tr ng-repeat="mapper in mappers | filter:search | orderBy:sortMappersByPriority">
|
||||
<td><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/mappers/{{mapper.id}}">{{mapper.name}}</a></td>
|
||||
<td>{{mapperTypes[mapper.protocolMapper].category}}</td>
|
||||
<td>{{mapperTypes[mapper.protocolMapper].name}}</td>
|
||||
<td>{{mapperTypes[mapper.protocolMapper].priority}}</td>
|
||||
<td class="kc-action-cell" kc-open="/realms/{{realm.realm}}/clients/{{client.id}}/mappers/{{mapper.id}}">{{:: 'edit' | translate}}</td>
|
||||
<td class="kc-action-cell" data-ng-show="client.access.manage" data-ng-click="removeMapper(mapper)">{{:: 'delete' | translate}}</td>
|
||||
</tr>
|
||||
|
|
|
@ -53,6 +53,13 @@
|
|||
</div>
|
||||
<kc-tooltip>{{:: 'client-scope.consent-screen-text.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group clearfix block" data-ng-show="protocol == 'openid-connect'">
|
||||
<label class="col-md-2 control-label" for="includeInTokenScope">{{:: 'client-scope.include-in-token-scope' | translate}}</label>
|
||||
<div class="col-sm-6">
|
||||
<input ng-model="includeInTokenScope" ng-click="switchChange()" name="displayOnConsentScreen" id="includeInTokenScope" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'client-scope.include-in-token-scope.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="guiOrder">{{:: 'client-scope.gui-order' | translate}} </label>
|
||||
<div class="col-sm-6">
|
||||
|
|
|
@ -32,14 +32,16 @@
|
|||
<th>{{:: 'name' | translate}}</th>
|
||||
<th>{{:: 'category' | translate}}</th>
|
||||
<th>{{:: 'type' | translate}}</th>
|
||||
<th>{{:: 'priority-order' | translate}}</th>
|
||||
<th colspan="2">{{:: 'actions' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="mapper in mappers | filter:search">
|
||||
<tr ng-repeat="mapper in mappers | filter:search | orderBy:sortMappersByPriority">
|
||||
<td><a href="#/realms/{{realm.realm}}/client-scopes/{{clientScope.id}}/mappers/{{mapper.id}}">{{mapper.name}}</a></td>
|
||||
<td>{{mapperTypes[mapper.protocolMapper].category}}</td>
|
||||
<td>{{mapperTypes[mapper.protocolMapper].name}}</td>
|
||||
<td>{{mapperTypes[mapper.protocolMapper].priority}}</td>
|
||||
<td class="kc-action-cell" kc-open="/realms/{{realm.realm}}/client-scopes/{{clientScope.id}}/mappers/{{mapper.id}}">{{:: 'edit' | translate}}</td>
|
||||
<td class="kc-action-cell" data-ng-click="removeMapper(mapper)">{{:: 'delete' | translate}}</td>
|
||||
</tr>
|
||||
|
|
|
@ -145,14 +145,16 @@
|
|||
<th>{{:: 'parent-client-scope' | translate}}</th>
|
||||
<th>{{:: 'category' | translate}}</th>
|
||||
<th>{{:: 'type' | translate}}</th>
|
||||
<th>{{:: 'priority-order' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="mapper in protocolMappers | filter:search">
|
||||
<tr ng-repeat="mapper in protocolMappers | filter:search | orderBy:sortMappersByPriority">
|
||||
<td><a href="#/realms/{{realm.realm}}/{{mapper.containerType}}s/{{mapper.containerId}}/mappers/{{mapper.mapperId}}">{{mapper.mapperName}}</a></td>
|
||||
<td><a href="#/realms/{{realm.realm}}/{{mapper.containerType}}s/{{mapper.containerId}}">{{mapper.containerName}}</a></td>
|
||||
<td>{{mapperTypes[mapper.protocolMapper].category}}</td>
|
||||
<td>{{mapperTypes[mapper.protocolMapper].name}}</td>
|
||||
<td>{{mapperTypes[mapper.protocolMapper].priority}}</td>
|
||||
</tr>
|
||||
<tr data-ng-show="mappers.length == 0">
|
||||
<td>{{:: 'no-mappers-available' | translate}}</td>
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue