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