Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CAS Username Mapper #49

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/main/java/org/keycloak/protocol/cas/CASLoginProtocol.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -57,7 +58,7 @@ public Response validate(String input) {

Map<String, Object> 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();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -28,7 +29,7 @@ public ServiceValidateEndpoint(RealmModel realm, EventBuilder event) {
protected Response successResponse() {
UserSessionModel userSession = clientSession.getUserSession();
Map<String, Object> 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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<String, Object> attributes, ProtocolMapperModel mappingModel, Object attributeValue) {
setPlainAttribute(attributes, mappingModel, OIDCAttributeMapperHelper.mapAttributeValue(mappingModel, attributeValue));
}

protected void setPlainAttribute(Map<String, Object> attributes, ProtocolMapperModel mappingModel, Object attributeValue) {
String protocolClaim = mappingModel.getConfig().get(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME);
if (protocolClaim == null || attributeValue == null) {
return;
}
attributes.put(protocolClaim, attributeValue);
}
}
Original file line number Diff line number Diff line change
@@ -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<String, Object> attributes, ProtocolMapperModel mappingModel, Object attributeValue) {
setPlainAttribute(attributes, mappingModel, OIDCAttributeMapperHelper.mapAttributeValue(mappingModel, attributeValue));
}

protected void setPlainAttribute(Map<String, Object> attributes, ProtocolMapperModel mappingModel, Object attributeValue) {
String protocolClaim = mappingModel.getConfig().get(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME);
if (protocolClaim == null || attributeValue == null) {
return;
}
attributes.put(protocolClaim, attributeValue);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
*
* @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
*/
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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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);

}
Original file line number Diff line number Diff line change
Expand Up @@ -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<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();

static {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();

private static final String FULL_PATH = "full.path";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();

public static final String CLAIM_VALUE = "claim.value";
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
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<ProviderConfigProperty> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();

static {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();

static {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,6 @@ public String getDisplayType() {
return "User Realm Role";
}

@Override
public String getDisplayCategory() {
return TOKEN_MAPPER_CATEGORY;
}

@Override
public String getHelpText() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();

Expand Down Expand Up @@ -45,10 +45,6 @@ public String getDisplayType() {
return "User Session Note";
}

@Override
public String getDisplayCategory() {
return TOKEN_MAPPER_CATEGORY;
}

@Override
public String getHelpText() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ProtocolMapperModel, ProtocolMapper> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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