mirror of https://github.com/jacekkow/keycloak-protocol-cas

Matthias Piepkorn
2017-01-27 513246cc7262ee2c63599608764cea538f6413f6
Add model for serviceResponse schema, implement attribute mappers
8 files modified
10 files added
532 ■■■■■ changed files
src/main/java/org/keycloak/protocol/cas/CASLoginProtocolService.java 5 ●●●● patch | view | raw | blame | history
src/main/java/org/keycloak/protocol/cas/endpoints/ServiceValidateEndpoint.java 38 ●●●●● patch | view | raw | blame | history
src/main/java/org/keycloak/protocol/cas/mappers/AbstractCASProtocolMapper.java 2 ●●● patch | view | raw | blame | history
src/main/java/org/keycloak/protocol/cas/mappers/CASAttributeMapper.java 10 ●●●●● patch | view | raw | blame | history
src/main/java/org/keycloak/protocol/cas/mappers/FullNameMapper.java 16 ●●●● patch | view | raw | blame | history
src/main/java/org/keycloak/protocol/cas/mappers/GroupMembershipMapper.java 25 ●●●● patch | view | raw | blame | history
src/main/java/org/keycloak/protocol/cas/mappers/HardcodedClaim.java 16 ●●●●● patch | view | raw | blame | history
src/main/java/org/keycloak/protocol/cas/mappers/UserAttributeMapper.java 16 ●●●●● patch | view | raw | blame | history
src/main/java/org/keycloak/protocol/cas/mappers/UserPropertyMapper.java 14 ●●●●● patch | view | raw | blame | history
src/main/java/org/keycloak/protocol/cas/representations/CasServiceResponse.java 25 ●●●●● patch | view | raw | blame | history
src/main/java/org/keycloak/protocol/cas/representations/CasServiceResponseAuthenticationFailure.java 30 ●●●●● patch | view | raw | blame | history
src/main/java/org/keycloak/protocol/cas/representations/CasServiceResponseAuthenticationSuccess.java 51 ●●●●● patch | view | raw | blame | history
src/main/java/org/keycloak/protocol/cas/representations/package-info.java 10 ●●●●● patch | view | raw | blame | history
src/main/java/org/keycloak/protocol/cas/utils/AttributesMapAdapter.java 57 ●●●●● patch | view | raw | blame | history
src/main/java/org/keycloak/protocol/cas/utils/ContentTypeHelper.java 27 ●●●●● patch | view | raw | blame | history
src/main/java/org/keycloak/protocol/cas/utils/ServiceResponseHelper.java 54 ●●●●● patch | view | raw | blame | history
src/main/java/org/keycloak/protocol/cas/utils/ServiceResponseMarshaller.java 58 ●●●●● patch | view | raw | blame | history
src/test/java/org/keycloak/protocol/cas/ServiceResponseTest.java 78 ●●●●● patch | view | raw | blame | history
src/main/java/org/keycloak/protocol/cas/CASLoginProtocolService.java
@@ -12,10 +12,7 @@
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;
src/main/java/org/keycloak/protocol/cas/endpoints/ServiceValidateEndpoint.java
@@ -6,16 +6,22 @@
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 @@
        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);
    }
}
src/main/java/org/keycloak/protocol/cas/mappers/AbstractCASProtocolMapper.java
@@ -6,7 +6,7 @@
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
src/main/java/org/keycloak/protocol/cas/mappers/CASAttributeMapper.java
New file
@@ -0,0 +1,10 @@
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);
}
src/main/java/org/keycloak/protocol/cas/mappers/FullNameMapper.java
@@ -1,7 +1,8 @@
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.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 @@
        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();
src/main/java/org/keycloak/protocol/cas/mappers/GroupMembershipMapper.java
@@ -1,15 +1,14 @@
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 @@
        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"));
    }
src/main/java/org/keycloak/protocol/cas/mappers/HardcodedClaim.java
@@ -1,10 +1,15 @@
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 @@
        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));
    }
}
src/main/java/org/keycloak/protocol/cas/mappers/UserAttributeMapper.java
@@ -1,6 +1,9 @@
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 @@
        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) {
src/main/java/org/keycloak/protocol/cas/mappers/UserPropertyMapper.java
@@ -1,6 +1,8 @@
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 @@
        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) {
src/main/java/org/keycloak/protocol/cas/representations/CasServiceResponse.java
New file
@@ -0,0 +1,25 @@
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;
    }
}
src/main/java/org/keycloak/protocol/cas/representations/CasServiceResponseAuthenticationFailure.java
New file
@@ -0,0 +1,30 @@
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;
    }
}
src/main/java/org/keycloak/protocol/cas/representations/CasServiceResponseAuthenticationSuccess.java
New file
@@ -0,0 +1,51 @@
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;
    }
}
src/main/java/org/keycloak/protocol/cas/representations/package-info.java
New file
@@ -0,0 +1,10 @@
@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;
src/main/java/org/keycloak/protocol/cas/utils/AttributesMapAdapter.java
New file
@@ -0,0 +1,57 @@
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()));
            }
        }
    }
}
src/main/java/org/keycloak/protocol/cas/utils/ContentTypeHelper.java
New file
@@ -0,0 +1,27 @@
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();
    }
}
src/main/java/org/keycloak/protocol/cas/utils/ServiceResponseHelper.java
New file
@@ -0,0 +1,54 @@
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();
        }
    }
}
src/main/java/org/keycloak/protocol/cas/utils/ServiceResponseMarshaller.java
New file
@@ -0,0 +1,58 @@
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(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
            StringWriter writer = new StringWriter();
            marshaller.marshal(serviceResponse, writer);
            return writer.toString();
        } catch (JAXBException e) {
            throw new RuntimeException(e);
        }
    }
    public static String marshalJson(CasServiceResponse serviceResponse) {
        ObjectMapper mapper = new ObjectMapper();
        mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        //Force newlines to be LF (default is system dependent)
        DefaultPrettyPrinter printer = new DefaultPrettyPrinter()
                .withObjectIndenter(new DefaultIndenter("  ", "\n"));
        //create wrapper node
        Map<String, Object> casModel = new HashMap<>();
        casModel.put("serviceResponse", serviceResponse);
        try {
            return mapper.writer(printer).writeValueAsString(casModel);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }
}
src/test/java/org/keycloak/protocol/cas/ServiceResponseTest.java
New file
@@ -0,0 +1,78 @@
package org.keycloak.protocol.cas;
import org.junit.Test;
import org.keycloak.protocol.cas.representations.CasServiceResponse;
import org.keycloak.protocol.cas.utils.ServiceResponseHelper;
import org.keycloak.protocol.cas.utils.ServiceResponseMarshaller;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import static org.junit.Assert.assertEquals;
public class ServiceResponseTest {
    private static final String EXPECTED_JSON_SUCCESS = "{\n" +
            "  \"serviceResponse\" : {\n" +
            "    \"authenticationSuccess\" : {\n" +
            "      \"user\" : \"username\",\n" +
            "      \"proxyGrantingTicket\" : \"PGTIOU-test\",\n" +
            "      \"proxies\" : [ \"https://proxy1/pgtUrl\", \"https://proxy2/pgtUrl\" ],\n" +
            "      \"attributes\" : {\n" +
            "        \"string\" : \"abc\",\n" +
            "        \"list\" : [ \"a\", \"b\" ],\n" +
            "        \"int\" : 123\n" +
            "      }\n" +
            "    }\n" +
            "  }\n" +
            "}";
    private static final String EXPECTED_XML_SUCCESS = "<cas:serviceResponse xmlns:cas=\"http://www.yale.edu/tp/cas\">\n" +
            "    <cas:authenticationSuccess>\n" +
            "        <cas:user>username</cas:user>\n" +
            "        <cas:proxyGrantingTicket>PGTIOU-test</cas:proxyGrantingTicket>\n" +
            "        <cas:proxies>\n" +
            "            <cas:proxy>https://proxy1/pgtUrl</cas:proxy>\n" +
            "            <cas:proxy>https://proxy2/pgtUrl</cas:proxy>\n" +
            "        </cas:proxies>\n" +
            "        <cas:attributes>\n" +
            "            <cas:string>abc</cas:string>\n" +
            "            <cas:list>a</cas:list>\n" +
            "            <cas:list>b</cas:list>\n" +
            "            <cas:int>123</cas:int>\n" +
            "        </cas:attributes>\n" +
            "    </cas:authenticationSuccess>\n" +
            "</cas:serviceResponse>";
    private static final String EXPECTED_JSON_FAILURE = "{\n" +
            "  \"serviceResponse\" : {\n" +
            "    \"authenticationFailure\" : {\n" +
            "      \"code\" : \"ERROR_CODE\",\n" +
            "      \"description\" : \"Error description\"\n" +
            "    }\n" +
            "  }\n" +
            "}";
    private static final String EXPECTED_XML_FAILURE = "<cas:serviceResponse xmlns:cas=\"http://www.yale.edu/tp/cas\">\n" +
            "    <cas:authenticationFailure code=\"ERROR_CODE\">Error description</cas:authenticationFailure>\n" +
            "</cas:serviceResponse>";
    @Test
    public void testSuccessResponse() throws Exception {
        Map<String, Object> attributes = new HashMap<>();
        attributes.put("list", Arrays.asList("a", "b"));
        attributes.put("int", 123);
        attributes.put("string", "abc");
        CasServiceResponse response = ServiceResponseHelper.createSuccess("username", attributes, "PGTIOU-test",
                Arrays.asList("https://proxy1/pgtUrl", "https://proxy2/pgtUrl"));
        assertEquals(EXPECTED_JSON_SUCCESS, ServiceResponseMarshaller.marshalJson(response));
        assertEquals(EXPECTED_XML_SUCCESS, ServiceResponseMarshaller.marshalXml(response));
    }
    @Test
    public void testErrorResponse() throws Exception {
        CasServiceResponse response = ServiceResponseHelper.createFailure("ERROR_CODE", "Error description");
        assertEquals(EXPECTED_JSON_FAILURE, ServiceResponseMarshaller.marshalJson(response));
        assertEquals(EXPECTED_XML_FAILURE, ServiceResponseMarshaller.marshalXml(response));
    }
}