From 513246cc7262ee2c63599608764cea538f6413f6 Mon Sep 17 00:00:00 2001
From: Matthias Piepkorn <mpiepk@gmail.com>
Date: Fri, 27 Jan 2017 22:48:27 +0000
Subject: [PATCH] Add model for serviceResponse schema, implement attribute mappers

---
 src/main/java/org/keycloak/protocol/cas/mappers/GroupMembershipMapper.java                           |   25 ++
 src/main/java/org/keycloak/protocol/cas/CASLoginProtocolService.java                                 |    5 
 src/main/java/org/keycloak/protocol/cas/mappers/HardcodedClaim.java                                  |   16 +
 src/main/java/org/keycloak/protocol/cas/utils/ServiceResponseMarshaller.java                         |   58 ++++++
 src/main/java/org/keycloak/protocol/cas/mappers/UserAttributeMapper.java                             |   16 +
 src/main/java/org/keycloak/protocol/cas/mappers/CASAttributeMapper.java                              |   10 +
 src/main/java/org/keycloak/protocol/cas/endpoints/ServiceValidateEndpoint.java                       |   38 ++-
 src/main/java/org/keycloak/protocol/cas/utils/ContentTypeHelper.java                                 |   27 +++
 src/main/java/org/keycloak/protocol/cas/representations/CasServiceResponseAuthenticationFailure.java |   30 +++
 src/main/java/org/keycloak/protocol/cas/mappers/UserPropertyMapper.java                              |   14 +
 src/test/java/org/keycloak/protocol/cas/ServiceResponseTest.java                                     |   78 ++++++++
 src/main/java/org/keycloak/protocol/cas/representations/CasServiceResponse.java                      |   25 ++
 src/main/java/org/keycloak/protocol/cas/representations/package-info.java                            |   10 +
 src/main/java/org/keycloak/protocol/cas/representations/CasServiceResponseAuthenticationSuccess.java |   51 +++++
 src/main/java/org/keycloak/protocol/cas/mappers/AbstractCASProtocolMapper.java                       |    2 
 src/main/java/org/keycloak/protocol/cas/utils/AttributesMapAdapter.java                              |   57 ++++++
 src/main/java/org/keycloak/protocol/cas/mappers/FullNameMapper.java                                  |   16 +
 src/main/java/org/keycloak/protocol/cas/utils/ServiceResponseHelper.java                             |   54 ++++++
 18 files changed, 503 insertions(+), 29 deletions(-)

diff --git a/src/main/java/org/keycloak/protocol/cas/CASLoginProtocolService.java b/src/main/java/org/keycloak/protocol/cas/CASLoginProtocolService.java
index 7db732f..9d3e27d 100644
--- a/src/main/java/org/keycloak/protocol/cas/CASLoginProtocolService.java
+++ b/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;
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 7b6a77e..b110d96 100644
--- a/src/main/java/org/keycloak/protocol/cas/endpoints/ServiceValidateEndpoint.java
+++ b/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);
     }
 }
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 8c61a4e..2c19433 100644
--- a/src/main/java/org/keycloak/protocol/cas/mappers/AbstractCASProtocolMapper.java
+++ b/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
diff --git a/src/main/java/org/keycloak/protocol/cas/mappers/CASAttributeMapper.java b/src/main/java/org/keycloak/protocol/cas/mappers/CASAttributeMapper.java
new file mode 100644
index 0000000..fccc99b
--- /dev/null
+++ b/src/main/java/org/keycloak/protocol/cas/mappers/CASAttributeMapper.java
@@ -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);
+}
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 59a9384..e66da7d 100644
--- a/src/main/java/org/keycloak/protocol/cas/mappers/FullNameMapper.java
+++ b/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();
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 665a6e8..6d9c8ac 100644
--- a/src/main/java/org/keycloak/protocol/cas/mappers/GroupMembershipMapper.java
+++ b/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"));
     }
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 34028ac..df7000c 100644
--- a/src/main/java/org/keycloak/protocol/cas/mappers/HardcodedClaim.java
+++ b/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));
+    }
+
 }
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 5206126..3637069 100644
--- a/src/main/java/org/keycloak/protocol/cas/mappers/UserAttributeMapper.java
+++ b/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) {
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 21cdeb6..057636d 100644
--- a/src/main/java/org/keycloak/protocol/cas/mappers/UserPropertyMapper.java
+++ b/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) {
diff --git a/src/main/java/org/keycloak/protocol/cas/representations/CasServiceResponse.java b/src/main/java/org/keycloak/protocol/cas/representations/CasServiceResponse.java
new file mode 100644
index 0000000..b43b52d
--- /dev/null
+++ b/src/main/java/org/keycloak/protocol/cas/representations/CasServiceResponse.java
@@ -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;
+    }
+}
diff --git a/src/main/java/org/keycloak/protocol/cas/representations/CasServiceResponseAuthenticationFailure.java b/src/main/java/org/keycloak/protocol/cas/representations/CasServiceResponseAuthenticationFailure.java
new file mode 100644
index 0000000..c34095c
--- /dev/null
+++ b/src/main/java/org/keycloak/protocol/cas/representations/CasServiceResponseAuthenticationFailure.java
@@ -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;
+    }
+}
diff --git a/src/main/java/org/keycloak/protocol/cas/representations/CasServiceResponseAuthenticationSuccess.java b/src/main/java/org/keycloak/protocol/cas/representations/CasServiceResponseAuthenticationSuccess.java
new file mode 100644
index 0000000..6dc8fd6
--- /dev/null
+++ b/src/main/java/org/keycloak/protocol/cas/representations/CasServiceResponseAuthenticationSuccess.java
@@ -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;
+    }
+}
diff --git a/src/main/java/org/keycloak/protocol/cas/representations/package-info.java b/src/main/java/org/keycloak/protocol/cas/representations/package-info.java
new file mode 100644
index 0000000..77a3ed5
--- /dev/null
+++ b/src/main/java/org/keycloak/protocol/cas/representations/package-info.java
@@ -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;
\ No newline at end of file
diff --git a/src/main/java/org/keycloak/protocol/cas/utils/AttributesMapAdapter.java b/src/main/java/org/keycloak/protocol/cas/utils/AttributesMapAdapter.java
new file mode 100644
index 0000000..bf9b148
--- /dev/null
+++ b/src/main/java/org/keycloak/protocol/cas/utils/AttributesMapAdapter.java
@@ -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()));
+            }
+        }
+    }
+}
diff --git a/src/main/java/org/keycloak/protocol/cas/utils/ContentTypeHelper.java b/src/main/java/org/keycloak/protocol/cas/utils/ContentTypeHelper.java
new file mode 100644
index 0000000..e842ef0
--- /dev/null
+++ b/src/main/java/org/keycloak/protocol/cas/utils/ContentTypeHelper.java
@@ -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();
+    }
+}
diff --git a/src/main/java/org/keycloak/protocol/cas/utils/ServiceResponseHelper.java b/src/main/java/org/keycloak/protocol/cas/utils/ServiceResponseHelper.java
new file mode 100644
index 0000000..8b927b8
--- /dev/null
+++ b/src/main/java/org/keycloak/protocol/cas/utils/ServiceResponseHelper.java
@@ -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();
+        }
+    }
+}
diff --git a/src/main/java/org/keycloak/protocol/cas/utils/ServiceResponseMarshaller.java b/src/main/java/org/keycloak/protocol/cas/utils/ServiceResponseMarshaller.java
new file mode 100644
index 0000000..a5aa6ac
--- /dev/null
+++ b/src/main/java/org/keycloak/protocol/cas/utils/ServiceResponseMarshaller.java
@@ -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);
+        }
+    }
+}
diff --git a/src/test/java/org/keycloak/protocol/cas/ServiceResponseTest.java b/src/test/java/org/keycloak/protocol/cas/ServiceResponseTest.java
new file mode 100644
index 0000000..349c0c8
--- /dev/null
+++ b/src/test/java/org/keycloak/protocol/cas/ServiceResponseTest.java
@@ -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));
+    }
+}

--
Gitblit v1.9.1