Commit 513246cc authored by Matthias Piepkorn's avatar Matthias Piepkorn
Browse files

Add model for serviceResponse schema, implement attribute mappers

parent 7f7e0cce
......@@ -12,10 +12,7 @@ import org.keycloak.protocol.cas.endpoints.ValidateEndpoint;
import org.keycloak.services.resources.RealmsResource;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.core.*;
public class CASLoginProtocolService {
private RealmModel realm;
......
......@@ -6,16 +6,22 @@ import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.ProtocolMapper;
import org.keycloak.protocol.cas.CASLoginProtocol;
import org.keycloak.protocol.cas.mappers.CASAttributeMapper;
import org.keycloak.protocol.cas.representations.CasServiceResponse;
import org.keycloak.protocol.cas.utils.ContentTypeHelper;
import org.keycloak.protocol.cas.utils.ServiceResponseHelper;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.managers.ClientSessionCode;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class ServiceValidateEndpoint extends ValidateEndpoint {
@Context
private Request restRequest;
public ServiceValidateEndpoint(RealmModel realm, EventBuilder event) {
super(realm, event);
}
......@@ -26,28 +32,26 @@ public class ServiceValidateEndpoint extends ValidateEndpoint {
Set<ProtocolMapperModel> mappings = new ClientSessionCode(session, realm, clientSession).getRequestedProtocolMappers();
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
Map<String, Object> attributes = new HashMap<>();
for (ProtocolMapperModel mapping : mappings) {
ProtocolMapper mapper = (ProtocolMapper) sessionFactory.getProviderFactory(ProtocolMapper.class, mapping.getProtocolMapper());
if (mapper instanceof CASAttributeMapper) {
((CASAttributeMapper) mapper).setAttribute(attributes, mapping, userSession);
}
}
return Response.ok()
.header(HttpHeaders.CONTENT_TYPE, (jsonFormat() ? MediaType.APPLICATION_JSON_TYPE : MediaType.APPLICATION_XML_TYPE).withCharset("utf-8"))
.entity("<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>\n" +
" <cas:authenticationSuccess>\n" +
" <cas:user>" + userSession.getUser().getUsername() + "</cas:user>\n" +
" <cas:attributes>\n" +
" </cas:attributes>\n" +
" </cas:authenticationSuccess>\n" +
"</cas:serviceResponse>")
.build();
CasServiceResponse serviceResponse = ServiceResponseHelper.createSuccess(userSession.getUser().getUsername(), attributes);
return prepare(Response.Status.OK, serviceResponse);
}
@Override
protected Response errorResponse(ErrorResponseException e) {
return super.errorResponse(e);
CasServiceResponse serviceResponse = ServiceResponseHelper.createFailure("CODE", "Description");
return prepare(Response.Status.FORBIDDEN, serviceResponse);
}
private boolean jsonFormat() {
return "json".equalsIgnoreCase(uriInfo.getQueryParameters().getFirst(CASLoginProtocol.FORMAT_PARAM));
private Response prepare(Response.Status status, CasServiceResponse serviceResponse) {
MediaType responseMediaType = new ContentTypeHelper(request, restRequest, uriInfo).selectResponseType();
return ServiceResponseHelper.createResponse(status, responseMediaType, serviceResponse);
}
}
......@@ -6,7 +6,7 @@ import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.protocol.ProtocolMapper;
import org.keycloak.protocol.cas.CASLoginProtocol;
public abstract class AbstractCASProtocolMapper implements ProtocolMapper {
public abstract class AbstractCASProtocolMapper implements ProtocolMapper, CASAttributeMapper {
public static final String TOKEN_MAPPER_CATEGORY = "Token mapper";
@Override
......
package org.keycloak.protocol.cas.mappers;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserSessionModel;
import java.util.Map;
public interface CASAttributeMapper {
void setAttribute(Map<String, Object> attributes, ProtocolMapperModel mappingModel, UserSessionModel userSession);
}
package org.keycloak.protocol.cas.mappers;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.protocol.ProtocolMapperUtils;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.cas.CASLoginProtocol;
import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
import org.keycloak.provider.ProviderConfigProperty;
......@@ -11,7 +12,6 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper.JSON_TYPE;
import static org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME;
public class FullNameMapper extends AbstractCASProtocolMapper {
......@@ -44,6 +44,18 @@ public class FullNameMapper extends AbstractCASProtocolMapper {
return "Maps the user's first and last name to the OpenID Connect 'name' claim. Format is <first> + ' ' + <last>";
}
@Override
public void setAttribute(Map<String, Object> attributes, ProtocolMapperModel mappingModel, UserSessionModel userSession) {
UserModel user = userSession.getUser();
String protocolClaim = mappingModel.getConfig().get(TOKEN_CLAIM_NAME);
if (protocolClaim == null) {
return;
}
String first = user.getFirstName() == null ? "" : user.getFirstName() + " ";
String last = user.getLastName() == null ? "" : user.getLastName();
attributes.put(protocolClaim, first + last);
}
public static ProtocolMapperModel create(String name, String tokenClaimName,
boolean consentRequired, String consentText) {
ProtocolMapperModel mapper = new ProtocolMapperModel();
......
package org.keycloak.protocol.cas.mappers;
import org.keycloak.models.GroupModel;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.protocol.cas.CASLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
import org.keycloak.provider.ProviderConfigProperty;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
public class GroupMembershipMapper extends AbstractCASProtocolMapper {
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
......@@ -48,6 +47,22 @@ public class GroupMembershipMapper extends AbstractCASProtocolMapper {
return "Map user group membership";
}
@Override
public void setAttribute(Map<String, Object> attributes, ProtocolMapperModel mappingModel, UserSessionModel userSession) {
List<String> membership = new LinkedList<>();
boolean fullPath = useFullPath(mappingModel);
for (GroupModel group : userSession.getUser().getGroups()) {
if (fullPath) {
membership.add(ModelToRepresentation.buildGroupPath(group));
} else {
membership.add(group.getName());
}
}
String protocolClaim = mappingModel.getConfig().get(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME);
attributes.put(protocolClaim, membership);
}
public static boolean useFullPath(ProtocolMapperModel mappingModel) {
return "true".equals(mappingModel.getConfig().get("full.path"));
}
......
package org.keycloak.protocol.cas.mappers;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
import org.keycloak.provider.ProviderConfigProperty;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME;
public class HardcodedClaim extends AbstractCASProtocolMapper {
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
......@@ -47,4 +52,15 @@ public class HardcodedClaim extends AbstractCASProtocolMapper {
return "Hardcode a claim into the token.";
}
@Override
public void setAttribute(Map<String, Object> attributes, ProtocolMapperModel mappingModel, UserSessionModel userSession) {
String protocolClaim = mappingModel.getConfig().get(TOKEN_CLAIM_NAME);
if (protocolClaim == null) {
return;
}
String attributeValue = mappingModel.getConfig().get(CLAIM_VALUE);
if (attributeValue == null) return;
attributes.put(protocolClaim, OIDCAttributeMapperHelper.mapAttributeValue(mappingModel, attributeValue));
}
}
package org.keycloak.protocol.cas.mappers;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.ProtocolMapperUtils;
import org.keycloak.protocol.cas.CASLoginProtocol;
import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
......@@ -60,6 +63,19 @@ public class UserAttributeMapper extends AbstractCASProtocolMapper {
return "Map a custom user attribute to a token claim.";
}
@Override
public void setAttribute(Map<String, Object> attributes, ProtocolMapperModel mappingModel, UserSessionModel userSession) {
UserModel user = userSession.getUser();
String protocolClaim = mappingModel.getConfig().get(TOKEN_CLAIM_NAME);
if (protocolClaim == null) {
return;
}
String attributeName = mappingModel.getConfig().get(ProtocolMapperUtils.USER_ATTRIBUTE);
List<String> attributeValue = KeycloakModelUtils.resolveAttribute(user, attributeName);
if (attributeValue == null) return;
attributes.put(protocolClaim, OIDCAttributeMapperHelper.mapAttributeValue(mappingModel, attributeValue));
}
public static ProtocolMapperModel create(String name, String userAttribute,
String tokenClaimName, String claimType,
boolean consentRequired, String consentText, boolean multivalued) {
......
package org.keycloak.protocol.cas.mappers;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.ProtocolMapperUtils;
import org.keycloak.protocol.cas.CASLoginProtocol;
import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
......@@ -52,6 +54,18 @@ public class UserPropertyMapper extends AbstractCASProtocolMapper {
return "Map a built in user property (email, firstName, lastName) to a token claim.";
}
@Override
public void setAttribute(Map<String, Object> attributes, ProtocolMapperModel mappingModel, UserSessionModel userSession) {
UserModel user = userSession.getUser();
String protocolClaim = mappingModel.getConfig().get(TOKEN_CLAIM_NAME);
if (protocolClaim == null) {
return;
}
String propertyName = mappingModel.getConfig().get(ProtocolMapperUtils.USER_ATTRIBUTE);
String propertyValue = ProtocolMapperUtils.getUserModelValue(user, propertyName);
attributes.put(protocolClaim, OIDCAttributeMapperHelper.mapAttributeValue(mappingModel, propertyValue));
}
public static ProtocolMapperModel create(String name, String userAttribute,
String tokenClaimName, String claimType,
boolean consentRequired, String consentText) {
......
package org.keycloak.protocol.cas.representations;
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement(name = "serviceResponse")
public class CasServiceResponse {
private CasServiceResponseAuthenticationFailure authenticationFailure;
private CasServiceResponseAuthenticationSuccess authenticationSuccess;
public CasServiceResponseAuthenticationFailure getAuthenticationFailure() {
return this.authenticationFailure;
}
public void setAuthenticationFailure(final CasServiceResponseAuthenticationFailure authenticationFailure) {
this.authenticationFailure = authenticationFailure;
}
public CasServiceResponseAuthenticationSuccess getAuthenticationSuccess() {
return this.authenticationSuccess;
}
public void setAuthenticationSuccess(final CasServiceResponseAuthenticationSuccess authenticationSuccess) {
this.authenticationSuccess = authenticationSuccess;
}
}
package org.keycloak.protocol.cas.representations;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlValue;
@XmlAccessorType(XmlAccessType.FIELD)
public class CasServiceResponseAuthenticationFailure {
@XmlAttribute
private String code;
@XmlValue
private String description;
public String getCode() {
return this.code;
}
public void setCode(final String code) {
this.code = code;
}
public String getDescription() {
return this.description;
}
public void setDescription(final String description) {
this.description = description;
}
}
package org.keycloak.protocol.cas.representations;
import org.keycloak.protocol.cas.utils.AttributesMapAdapter;
import javax.xml.bind.annotation.*;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import java.util.List;
import java.util.Map;
@XmlAccessorType(XmlAccessType.FIELD)
public class CasServiceResponseAuthenticationSuccess {
private String user;
private String proxyGrantingTicket;
@XmlElementWrapper
@XmlElement(name="proxy")
private List<String> proxies;
@XmlJavaTypeAdapter(AttributesMapAdapter.class)
private Map<String, Object> attributes;
public String getUser() {
return this.user;
}
public void setUser(final String user) {
this.user = user;
}
public String getProxyGrantingTicket() {
return this.proxyGrantingTicket;
}
public void setProxyGrantingTicket(final String proxyGrantingTicket) {
this.proxyGrantingTicket = proxyGrantingTicket;
}
public List<String> getProxies() {
return this.proxies;
}
public void setProxies(final List<String> proxies) {
this.proxies = proxies;
}
public Map<String, Object> getAttributes() {
return this.attributes;
}
public void setAttributes(final Map<String, Object> attributes) {
this.attributes = attributes;
}
}
@XmlSchema(
namespace = "http://www.yale.edu/tp/cas",
xmlns = {
@XmlNs(namespaceURI = "http://www.yale.edu/tp/cas", prefix = "cas")
},
elementFormDefault = javax.xml.bind.annotation.XmlNsForm.QUALIFIED)
package org.keycloak.protocol.cas.representations;
import javax.xml.bind.annotation.XmlNs;
import javax.xml.bind.annotation.XmlSchema;
\ No newline at end of file
package org.keycloak.protocol.cas.utils;
import org.keycloak.protocol.cas.representations.CasServiceResponse;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAnyElement;
import javax.xml.bind.annotation.XmlSchema;
import javax.xml.bind.annotation.adapters.XmlAdapter;
import javax.xml.namespace.QName;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Transforms the attribute map of the AuthenticationSuccess object (which can contain either simple values or
* lists) to a flat list of XML nodes, where the key is the node name.<br>
* Lists output multiple XML nodes with the same name.
*/
public final class AttributesMapAdapter extends XmlAdapter<AttributesMapAdapter.AttributeWrapperType, Map<String, Object>> {
@Override
public AttributeWrapperType marshal(Map<String, Object> v) throws Exception {
return new AttributeWrapperType(v);
}
@Override
public Map<String, Object> unmarshal(AttributeWrapperType v) throws Exception {
throw new IllegalStateException("not implemented");
}
@XmlAccessorType(XmlAccessType.FIELD)
static class AttributeWrapperType {
@XmlAnyElement
private final List<JAXBElement<String>> elements;
AttributeWrapperType(Map<String, Object> attributes) {
this.elements = new ArrayList<>();
for (Map.Entry<String, Object> entry : attributes.entrySet()) {
if (entry.getValue() instanceof List) {
for (Object item : ((List) entry.getValue())) {
addElement(entry.getKey(), item);
}
} else {
addElement(entry.getKey(), entry.getValue());
}
}
}
private void addElement(String name, Object value) {
if (value != null) {
String namespace = CasServiceResponse.class.getPackage().getAnnotation(XmlSchema.class).namespace();
elements.add(new JAXBElement<>(new QName(namespace, name), String.class, value.toString()));
}
}
}
}
package org.keycloak.protocol.cas.utils;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.protocol.cas.CASLoginProtocol;
import javax.ws.rs.core.*;
public class ContentTypeHelper {
private final HttpRequest request;
private final Request restRequest;
private final UriInfo uriInfo;
public ContentTypeHelper(HttpRequest request, Request restRequest, UriInfo uriInfo) {
this.request = request;
this.restRequest = restRequest;
this.uriInfo = uriInfo;
}
public MediaType selectResponseType() {
String format = uriInfo.getQueryParameters().getFirst(CASLoginProtocol.FORMAT_PARAM);
if (format != null && !format.isEmpty()) {
request.getMutableHeaders().add(HttpHeaders.ACCEPT, "application/" + format);
}
Variant variant = restRequest.selectVariant(Variant.mediaTypes(MediaType.APPLICATION_XML_TYPE, MediaType.APPLICATION_JSON_TYPE).build());
return variant == null ? MediaType.APPLICATION_XML_TYPE : variant.getMediaType();
}
}
package org.keycloak.protocol.cas.utils;
import org.keycloak.protocol.cas.representations.CasServiceResponse;
import org.keycloak.protocol.cas.representations.CasServiceResponseAuthenticationFailure;
import org.keycloak.protocol.cas.representations.CasServiceResponseAuthenticationSuccess;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;
import java.util.Map;
public final class ServiceResponseHelper {
private ServiceResponseHelper() {
}
public static CasServiceResponse createSuccess(String username, Map<String, Object> attributes) {
return createSuccess(username, attributes, null, null);
}
public static CasServiceResponse createSuccess(String username, Map<String, Object> attributes,
String proxyGrantingTicket, List<String> proxies) {
CasServiceResponse response = new CasServiceResponse();
CasServiceResponseAuthenticationSuccess success = new CasServiceResponseAuthenticationSuccess();
success.setUser(username);
success.setProxies(proxies);
success.setProxyGrantingTicket(proxyGrantingTicket);
success.setAttributes(attributes);
response.setAuthenticationSuccess(success);
return response;
}
public static CasServiceResponse createFailure(String errorCode, String errorDescription) {
CasServiceResponse response = new CasServiceResponse();
CasServiceResponseAuthenticationFailure failure = new CasServiceResponseAuthenticationFailure();
failure.setCode(errorCode);
failure.setDescription(errorDescription);
response.setAuthenticationFailure(failure);
return response;
}
public static Response createResponse(Response.Status status, MediaType mediaType, CasServiceResponse serviceResponse) {
Response.ResponseBuilder builder = Response.status(status)
.header(HttpHeaders.CONTENT_TYPE, mediaType.withCharset("utf-8"));
if (MediaType.APPLICATION_JSON_TYPE.equals(mediaType)) {
return builder.entity(ServiceResponseMarshaller.marshalJson(serviceResponse)).build();
} else {
return builder.entity(ServiceResponseMarshaller.marshalXml(serviceResponse)).build();
}
}
}
package org.keycloak.protocol.cas.utils;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.util.DefaultIndenter;
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.keycloak.protocol.cas.representations.CasServiceResponse;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
/**
* Helper methods to marshal service response object to XML/JSON<br
* For details on expected format see CAS-Protocol-Specification.html, section 2.5/2.6
*/
public final class ServiceResponseMarshaller {
private ServiceResponseMarshaller() {
}
public static String marshalXml(CasServiceResponse serviceResponse) {
try {
JAXBContext jaxbContext = JAXBContext.newInstance(CasServiceResponse.class);
Marshaller marshaller = jaxbContext.createMarshaller();
//disable xml header
marshaller.setProperty(Marshaller.JAXB_FRAGMENT, true);
marshaller.setProperty(Marshaller.JAXB_ENCODING, StandardCharsets.UTF_8.name());
marshaller.setProperty<