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