From b970ad1c80ddf293beeee260eeb90a92c6c7b6e2 Mon Sep 17 00:00:00 2001 From: Daniel Ramos Date: Tue, 29 Mar 2022 11:59:40 -0400 Subject: [PATCH 1/3] reorg: Extract AbstractUserAttributeMapper from AbstractCASProtocolMapper --- .../mappers/AbstractCASProtocolMapper.java | 25 +---------------- .../mappers/AbstractUserAttributeMapper.java | 27 +++++++++++++++++++ .../AbstractUserRoleMappingMapper.java | 2 +- .../protocol/cas/mappers/FullNameMapper.java | 2 +- .../cas/mappers/GroupMembershipMapper.java | 2 +- .../protocol/cas/mappers/HardcodedClaim.java | 4 +-- .../cas/mappers/UserAttributeMapper.java | 2 +- .../cas/mappers/UserPropertyMapper.java | 2 +- .../cas/mappers/UserSessionNoteMapper.java | 2 +- 9 files changed, 35 insertions(+), 33 deletions(-) create mode 100644 src/main/java/org/keycloak/protocol/cas/mappers/AbstractUserAttributeMapper.java diff --git a/src/main/java/org/keycloak/protocol/cas/mappers/AbstractCASProtocolMapper.java b/src/main/java/org/keycloak/protocol/cas/mappers/AbstractCASProtocolMapper.java index 6838f6d..64ed6bf 100644 --- a/src/main/java/org/keycloak/protocol/cas/mappers/AbstractCASProtocolMapper.java +++ b/src/main/java/org/keycloak/protocol/cas/mappers/AbstractCASProtocolMapper.java @@ -3,16 +3,10 @@ import org.keycloak.Config; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; -import org.keycloak.models.ProtocolMapperModel; import org.keycloak.protocol.ProtocolMapper; import org.keycloak.protocol.cas.CASLoginProtocol; -import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper; - -import java.util.Map; - -public abstract class AbstractCASProtocolMapper implements ProtocolMapper, CASAttributeMapper { - public static final String TOKEN_MAPPER_CATEGORY = "Token mapper"; +public abstract class AbstractCASProtocolMapper implements ProtocolMapper { @Override public String getProtocol() { return CASLoginProtocol.LOGIN_PROTOCOL; @@ -34,21 +28,4 @@ public void init(Config.Scope config) { @Override public void postInit(KeycloakSessionFactory factory) { } - - @Override - public String getDisplayCategory() { - return TOKEN_MAPPER_CATEGORY; - } - - protected void setMappedAttribute(Map attributes, ProtocolMapperModel mappingModel, Object attributeValue) { - setPlainAttribute(attributes, mappingModel, OIDCAttributeMapperHelper.mapAttributeValue(mappingModel, attributeValue)); - } - - protected void setPlainAttribute(Map attributes, ProtocolMapperModel mappingModel, Object attributeValue) { - String protocolClaim = mappingModel.getConfig().get(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME); - if (protocolClaim == null || attributeValue == null) { - return; - } - attributes.put(protocolClaim, attributeValue); - } } diff --git a/src/main/java/org/keycloak/protocol/cas/mappers/AbstractUserAttributeMapper.java b/src/main/java/org/keycloak/protocol/cas/mappers/AbstractUserAttributeMapper.java new file mode 100644 index 0000000..db451bc --- /dev/null +++ b/src/main/java/org/keycloak/protocol/cas/mappers/AbstractUserAttributeMapper.java @@ -0,0 +1,27 @@ +package org.keycloak.protocol.cas.mappers; + +import org.keycloak.models.ProtocolMapperModel; +import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper; + +import java.util.Map; + +public abstract class AbstractUserAttributeMapper extends AbstractCASProtocolMapper implements CASAttributeMapper { + public static final String TOKEN_MAPPER_CATEGORY = "Token mapper"; + + @Override + public String getDisplayCategory() { + return TOKEN_MAPPER_CATEGORY; + } + + protected void setMappedAttribute(Map attributes, ProtocolMapperModel mappingModel, Object attributeValue) { + setPlainAttribute(attributes, mappingModel, OIDCAttributeMapperHelper.mapAttributeValue(mappingModel, attributeValue)); + } + + protected void setPlainAttribute(Map attributes, ProtocolMapperModel mappingModel, Object attributeValue) { + String protocolClaim = mappingModel.getConfig().get(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME); + if (protocolClaim == null || attributeValue == null) { + return; + } + attributes.put(protocolClaim, attributeValue); + } +} diff --git a/src/main/java/org/keycloak/protocol/cas/mappers/AbstractUserRoleMappingMapper.java b/src/main/java/org/keycloak/protocol/cas/mappers/AbstractUserRoleMappingMapper.java index 4408d7d..0357230 100644 --- a/src/main/java/org/keycloak/protocol/cas/mappers/AbstractUserRoleMappingMapper.java +++ b/src/main/java/org/keycloak/protocol/cas/mappers/AbstractUserRoleMappingMapper.java @@ -28,7 +28,7 @@ * * @author Thomas Darimont */ -abstract class AbstractUserRoleMappingMapper extends AbstractCASProtocolMapper { +abstract class AbstractUserRoleMappingMapper extends AbstractUserAttributeMapper { /** * Retrieves all roles of the current user based on direct roles set to the user, its groups and their parent groups. diff --git a/src/main/java/org/keycloak/protocol/cas/mappers/FullNameMapper.java b/src/main/java/org/keycloak/protocol/cas/mappers/FullNameMapper.java index baf8257..6c346da 100644 --- a/src/main/java/org/keycloak/protocol/cas/mappers/FullNameMapper.java +++ b/src/main/java/org/keycloak/protocol/cas/mappers/FullNameMapper.java @@ -8,7 +8,7 @@ import java.util.List; import java.util.Map; -public class FullNameMapper extends AbstractCASProtocolMapper { +public class FullNameMapper extends AbstractUserAttributeMapper { private static final List configProperties = new ArrayList(); static { diff --git a/src/main/java/org/keycloak/protocol/cas/mappers/GroupMembershipMapper.java b/src/main/java/org/keycloak/protocol/cas/mappers/GroupMembershipMapper.java index e5a9a89..7250fe1 100644 --- a/src/main/java/org/keycloak/protocol/cas/mappers/GroupMembershipMapper.java +++ b/src/main/java/org/keycloak/protocol/cas/mappers/GroupMembershipMapper.java @@ -10,7 +10,7 @@ import java.util.List; import java.util.Map; -public class GroupMembershipMapper extends AbstractCASProtocolMapper { +public class GroupMembershipMapper extends AbstractUserAttributeMapper { private static final List configProperties = new ArrayList(); private static final String FULL_PATH = "full.path"; diff --git a/src/main/java/org/keycloak/protocol/cas/mappers/HardcodedClaim.java b/src/main/java/org/keycloak/protocol/cas/mappers/HardcodedClaim.java index f66c469..2f6252c 100644 --- a/src/main/java/org/keycloak/protocol/cas/mappers/HardcodedClaim.java +++ b/src/main/java/org/keycloak/protocol/cas/mappers/HardcodedClaim.java @@ -11,9 +11,7 @@ import java.util.List; import java.util.Map; -import static org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME; - -public class HardcodedClaim extends AbstractCASProtocolMapper { +public class HardcodedClaim extends AbstractUserAttributeMapper { private static final List configProperties = new ArrayList(); public static final String CLAIM_VALUE = "claim.value"; diff --git a/src/main/java/org/keycloak/protocol/cas/mappers/UserAttributeMapper.java b/src/main/java/org/keycloak/protocol/cas/mappers/UserAttributeMapper.java index 1ec125d..e624e29 100644 --- a/src/main/java/org/keycloak/protocol/cas/mappers/UserAttributeMapper.java +++ b/src/main/java/org/keycloak/protocol/cas/mappers/UserAttributeMapper.java @@ -11,7 +11,7 @@ import java.util.List; import java.util.Map; -public class UserAttributeMapper extends AbstractCASProtocolMapper { +public class UserAttributeMapper extends AbstractUserAttributeMapper { private static final List configProperties = new ArrayList(); static { diff --git a/src/main/java/org/keycloak/protocol/cas/mappers/UserPropertyMapper.java b/src/main/java/org/keycloak/protocol/cas/mappers/UserPropertyMapper.java index 24c9af8..089746a 100644 --- a/src/main/java/org/keycloak/protocol/cas/mappers/UserPropertyMapper.java +++ b/src/main/java/org/keycloak/protocol/cas/mappers/UserPropertyMapper.java @@ -9,7 +9,7 @@ import java.util.List; import java.util.Map; -public class UserPropertyMapper extends AbstractCASProtocolMapper { +public class UserPropertyMapper extends AbstractUserAttributeMapper { private static final List configProperties = new ArrayList(); static { diff --git a/src/main/java/org/keycloak/protocol/cas/mappers/UserSessionNoteMapper.java b/src/main/java/org/keycloak/protocol/cas/mappers/UserSessionNoteMapper.java index f79c1f7..4620a8f 100644 --- a/src/main/java/org/keycloak/protocol/cas/mappers/UserSessionNoteMapper.java +++ b/src/main/java/org/keycloak/protocol/cas/mappers/UserSessionNoteMapper.java @@ -12,7 +12,7 @@ import java.util.List; import java.util.Map; -public class UserSessionNoteMapper extends AbstractCASProtocolMapper { +public class UserSessionNoteMapper extends AbstractUserAttributeMapper { private static final List configProperties = new ArrayList(); From 04605e6bf50fc2a08e97b2e9148f9efc20686ec1 Mon Sep 17 00:00:00 2001 From: Daniel Ramos Date: Tue, 29 Mar 2022 12:04:10 -0400 Subject: [PATCH 2/3] cleanup: Delete duplicated overridden methods that are equivalent to superclass implementation in several mappers --- .../protocol/cas/mappers/UserClientRoleMappingMapper.java | 5 ----- .../protocol/cas/mappers/UserRealmRoleMappingMapper.java | 4 ---- .../keycloak/protocol/cas/mappers/UserSessionNoteMapper.java | 4 ---- 3 files changed, 13 deletions(-) diff --git a/src/main/java/org/keycloak/protocol/cas/mappers/UserClientRoleMappingMapper.java b/src/main/java/org/keycloak/protocol/cas/mappers/UserClientRoleMappingMapper.java index 1b236e4..6274251 100644 --- a/src/main/java/org/keycloak/protocol/cas/mappers/UserClientRoleMappingMapper.java +++ b/src/main/java/org/keycloak/protocol/cas/mappers/UserClientRoleMappingMapper.java @@ -50,11 +50,6 @@ public String getDisplayType() { return "User Client Role"; } - @Override - public String getDisplayCategory() { - return TOKEN_MAPPER_CATEGORY; - } - @Override public String getHelpText() { return "Map a user client role to a token claim."; diff --git a/src/main/java/org/keycloak/protocol/cas/mappers/UserRealmRoleMappingMapper.java b/src/main/java/org/keycloak/protocol/cas/mappers/UserRealmRoleMappingMapper.java index 1c3094f..4cd52f1 100644 --- a/src/main/java/org/keycloak/protocol/cas/mappers/UserRealmRoleMappingMapper.java +++ b/src/main/java/org/keycloak/protocol/cas/mappers/UserRealmRoleMappingMapper.java @@ -46,10 +46,6 @@ public String getDisplayType() { return "User Realm Role"; } - @Override - public String getDisplayCategory() { - return TOKEN_MAPPER_CATEGORY; - } @Override public String getHelpText() { diff --git a/src/main/java/org/keycloak/protocol/cas/mappers/UserSessionNoteMapper.java b/src/main/java/org/keycloak/protocol/cas/mappers/UserSessionNoteMapper.java index 4620a8f..aa30af2 100644 --- a/src/main/java/org/keycloak/protocol/cas/mappers/UserSessionNoteMapper.java +++ b/src/main/java/org/keycloak/protocol/cas/mappers/UserSessionNoteMapper.java @@ -45,10 +45,6 @@ public String getDisplayType() { return "User Session Note"; } - @Override - public String getDisplayCategory() { - return TOKEN_MAPPER_CATEGORY; - } @Override public String getHelpText() { From 3955f779e420c6aac3adad94405d97c63a95478c Mon Sep 17 00:00:00 2001 From: Daniel Ramos Date: Tue, 29 Mar 2022 16:47:10 -0400 Subject: [PATCH 3/3] Add a Username Mapper to allow a user attribute to be mapped to the CAS username response --- .../protocol/cas/CASLoginProtocol.java | 6 ++ .../cas/endpoints/SamlValidateEndpoint.java | 3 +- .../endpoints/ServiceValidateEndpoint.java | 3 +- .../cas/mappers/CASUsernameMapper.java | 11 +++ .../UserAttributeCasUsernameMapper.java | 77 +++++++++++++++++++ .../cas/utils/UsernameMapperHelper.java | 32 ++++++++ .../org.keycloak.protocol.ProtocolMapper | 1 + 7 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/keycloak/protocol/cas/mappers/CASUsernameMapper.java create mode 100644 src/main/java/org/keycloak/protocol/cas/mappers/UserAttributeCasUsernameMapper.java create mode 100644 src/main/java/org/keycloak/protocol/cas/utils/UsernameMapperHelper.java diff --git a/src/main/java/org/keycloak/protocol/cas/CASLoginProtocol.java b/src/main/java/org/keycloak/protocol/cas/CASLoginProtocol.java index 14f276a..388099d 100644 --- a/src/main/java/org/keycloak/protocol/cas/CASLoginProtocol.java +++ b/src/main/java/org/keycloak/protocol/cas/CASLoginProtocol.java @@ -10,6 +10,7 @@ import org.keycloak.models.*; import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.cas.utils.LogoutHelper; +import org.keycloak.protocol.cas.utils.UsernameMapperHelper; import org.keycloak.protocol.oidc.utils.OAuth2Code; import org.keycloak.protocol.oidc.utils.OAuth2CodeParser; import org.keycloak.services.ErrorPage; @@ -93,6 +94,11 @@ public CASLoginProtocol setEventBuilder(EventBuilder event) { public Response authenticated(AuthenticationSessionModel authSession, UserSessionModel userSession, ClientSessionContext clientSessionCtx) { AuthenticatedClientSessionModel clientSession = clientSessionCtx.getClientSession(); + // Verify that if a username mapper is set - there is a username being returned + if(UsernameMapperHelper.getMappedUsername(session, clientSession) == null) { + return ErrorPage.error(session, authSession, Response.Status.INTERNAL_SERVER_ERROR, "Unable to map username for CAS client"); + } + String service = authSession.getRedirectUri(); //TODO validate service diff --git a/src/main/java/org/keycloak/protocol/cas/endpoints/SamlValidateEndpoint.java b/src/main/java/org/keycloak/protocol/cas/endpoints/SamlValidateEndpoint.java index 3d7f3c3..3abc0e3 100644 --- a/src/main/java/org/keycloak/protocol/cas/endpoints/SamlValidateEndpoint.java +++ b/src/main/java/org/keycloak/protocol/cas/endpoints/SamlValidateEndpoint.java @@ -9,6 +9,7 @@ import org.keycloak.protocol.cas.representations.CASErrorCode; import org.keycloak.protocol.cas.representations.SamlResponseHelper; import org.keycloak.protocol.cas.utils.CASValidationException; +import org.keycloak.protocol.cas.utils.UsernameMapperHelper; import org.keycloak.services.Urls; import org.xml.sax.InputSource; @@ -57,7 +58,7 @@ public Response validate(String input) { Map attributes = getUserAttributes(); - SAML11ResponseType response = SamlResponseHelper.successResponse(issuer, user.getUsername(), attributes); + SAML11ResponseType response = SamlResponseHelper.successResponse(issuer, UsernameMapperHelper.getMappedUsername(session,clientSession), attributes); return Response.ok(SamlResponseHelper.soap(response)).build(); diff --git a/src/main/java/org/keycloak/protocol/cas/endpoints/ServiceValidateEndpoint.java b/src/main/java/org/keycloak/protocol/cas/endpoints/ServiceValidateEndpoint.java index fa56d4f..c7c1366 100644 --- a/src/main/java/org/keycloak/protocol/cas/endpoints/ServiceValidateEndpoint.java +++ b/src/main/java/org/keycloak/protocol/cas/endpoints/ServiceValidateEndpoint.java @@ -8,6 +8,7 @@ import org.keycloak.protocol.cas.utils.CASValidationException; import org.keycloak.protocol.cas.utils.ContentTypeHelper; import org.keycloak.protocol.cas.utils.ServiceResponseHelper; +import org.keycloak.protocol.cas.utils.UsernameMapperHelper; import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.util.DefaultClientSessionContext; @@ -28,7 +29,7 @@ public ServiceValidateEndpoint(RealmModel realm, EventBuilder event) { protected Response successResponse() { UserSessionModel userSession = clientSession.getUserSession(); Map attributes = getUserAttributes(); - CASServiceResponse serviceResponse = ServiceResponseHelper.createSuccess(userSession.getUser().getUsername(), attributes); + CASServiceResponse serviceResponse = ServiceResponseHelper.createSuccess(UsernameMapperHelper.getMappedUsername(session,clientSession), attributes); return prepare(Response.Status.OK, serviceResponse); } diff --git a/src/main/java/org/keycloak/protocol/cas/mappers/CASUsernameMapper.java b/src/main/java/org/keycloak/protocol/cas/mappers/CASUsernameMapper.java new file mode 100644 index 0000000..848e05e --- /dev/null +++ b/src/main/java/org/keycloak/protocol/cas/mappers/CASUsernameMapper.java @@ -0,0 +1,11 @@ +package org.keycloak.protocol.cas.mappers; + +import org.keycloak.models.*; +import org.keycloak.protocol.ProtocolMapper; + +public interface CASUsernameMapper extends ProtocolMapper { + + String getMappedUsername(ProtocolMapperModel mappingModel, KeycloakSession session, + UserSessionModel userSession, AuthenticatedClientSessionModel clientSession); + +} diff --git a/src/main/java/org/keycloak/protocol/cas/mappers/UserAttributeCasUsernameMapper.java b/src/main/java/org/keycloak/protocol/cas/mappers/UserAttributeCasUsernameMapper.java new file mode 100644 index 0000000..d231e0e --- /dev/null +++ b/src/main/java/org/keycloak/protocol/cas/mappers/UserAttributeCasUsernameMapper.java @@ -0,0 +1,77 @@ +package org.keycloak.protocol.cas.mappers; + +import org.keycloak.Config; +import org.keycloak.models.*; +import org.keycloak.protocol.ProtocolMapper; +import org.keycloak.protocol.ProtocolMapperUtils; +import org.keycloak.protocol.cas.CASLoginProtocol; +import org.keycloak.provider.ProviderConfigProperty; + +import java.util.ArrayList; +import java.util.List; + +public class UserAttributeCasUsernameMapper extends AbstractCASProtocolMapper implements CASUsernameMapper { + public static final String PROVIDER_ID = "cas-usermodel-username-mapper"; + public static final String USERNAME_MAPPER_CATEGORY = "CAS Username Mapper"; + private static final String CONF_FALLBACK_TO_USERNAME_IF_NULL = "username_fallback"; + + private static final List configProperties = new ArrayList(); + static { + ProviderConfigProperty property; + property = new ProviderConfigProperty(); + property.setName(ProtocolMapperUtils.USER_ATTRIBUTE); + property.setLabel(ProtocolMapperUtils.USER_MODEL_PROPERTY_LABEL); + property.setType(ProviderConfigProperty.STRING_TYPE); + property.setHelpText(ProtocolMapperUtils.USER_MODEL_PROPERTY_HELP_TEXT); + configProperties.add(property); + + property = new ProviderConfigProperty(); + property.setName(CONF_FALLBACK_TO_USERNAME_IF_NULL); + property.setLabel("Use username if attribute is missing"); + property.setHelpText("Should the User's username be used if the specified attribute is blank?"); + property.setType(ProviderConfigProperty.BOOLEAN_TYPE); + property.setDefaultValue(false); + configProperties.add(property); + + + } + + @Override + public final String getDisplayCategory() { + return USERNAME_MAPPER_CATEGORY; + } + + @Override + public final String getId() { + return PROVIDER_ID; + } + + @Override + public String getDisplayType() { + return "User Attribute Mapper For CAS Username"; + } + + @Override + public String getHelpText() { + return "Maps a user attribute to CAS Username value."; + } + + @Override + public List getConfigProperties() { + return configProperties; + } + + @Override + public String getMappedUsername(ProtocolMapperModel mappingModel, KeycloakSession session, + UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { + + boolean defaultIfNull = Boolean.parseBoolean(mappingModel.getConfig().get(CONF_FALLBACK_TO_USERNAME_IF_NULL)); + UserModel user = userSession.getUser(); + String mappedUsername = user.getFirstAttribute(mappingModel.getConfig().get(ProtocolMapperUtils.USER_ATTRIBUTE)); + + if(mappedUsername == null && defaultIfNull) { + mappedUsername = user.getUsername(); + } + return mappedUsername; + } +} diff --git a/src/main/java/org/keycloak/protocol/cas/utils/UsernameMapperHelper.java b/src/main/java/org/keycloak/protocol/cas/utils/UsernameMapperHelper.java new file mode 100644 index 0000000..c8617e1 --- /dev/null +++ b/src/main/java/org/keycloak/protocol/cas/utils/UsernameMapperHelper.java @@ -0,0 +1,32 @@ +package org.keycloak.protocol.cas.utils; + +import org.keycloak.models.*; +import org.keycloak.protocol.ProtocolMapper; +import org.keycloak.protocol.ProtocolMapperUtils; +import org.keycloak.protocol.cas.mappers.CASUsernameMapper; +import org.keycloak.services.util.DefaultClientSessionContext; + +import java.util.Map; + +public class UsernameMapperHelper { + public static String getMappedUsername(KeycloakSession session, AuthenticatedClientSessionModel clientSession) { + // CAS protocol does not support scopes, so pass null scopeParam + ClientSessionContext clientSessionCtx = DefaultClientSessionContext.fromClientSessionAndScopeParameter(clientSession, null, session); + UserSessionModel userSession = clientSession.getUserSession(); + + + Map.Entry mapperPair = ProtocolMapperUtils.getSortedProtocolMappers(session,clientSessionCtx) + .filter(e -> e.getValue() instanceof CASUsernameMapper) + .findFirst() + .orElse(null); + + String mappedUsername = userSession.getUser().getUsername(); + + if(mapperPair != null) { + ProtocolMapperModel mapping = mapperPair.getKey(); + CASUsernameMapper casUsernameMapper = (CASUsernameMapper) mapperPair.getValue(); + mappedUsername = casUsernameMapper.getMappedUsername(mapping, session, userSession, clientSession); + } + return mappedUsername; + } +} diff --git a/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper b/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper index 353f1c8..17f03ec 100644 --- a/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper +++ b/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper @@ -23,3 +23,4 @@ org.keycloak.protocol.cas.mappers.UserClientRoleMappingMapper org.keycloak.protocol.cas.mappers.UserPropertyMapper org.keycloak.protocol.cas.mappers.UserRealmRoleMappingMapper org.keycloak.protocol.cas.mappers.UserSessionNoteMapper +org.keycloak.protocol.cas.mappers.UserAttributeCasUsernameMapper