Add model for serviceResponse schema, implement attribute mappers
8 files modified
10 files added
| | |
| | | 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; |
| | |
| | | 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); |
| | | } |
| | |
| | | |
| | | 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); |
| | | } |
| | | } |
| | |
| | | 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 |
New file |
| | |
| | | package org.keycloak.protocol.cas.mappers; |
| | | |
| | | import org.keycloak.models.ProtocolMapperModel; |
| | | import org.keycloak.models.UserSessionModel; |
| | | |
| | | import java.util.Map; |
| | | |
| | | public interface CASAttributeMapper { |
| | | void setAttribute(Map<String, Object> attributes, ProtocolMapperModel mappingModel, UserSessionModel userSession); |
| | | } |
| | |
| | | package org.keycloak.protocol.cas.mappers; |
| | | |
| | | import org.keycloak.models.ProtocolMapperModel; |
| | | import org.keycloak.protocol.ProtocolMapperUtils; |
| | | import org.keycloak.models.UserModel; |
| | | import org.keycloak.models.UserSessionModel; |
| | | import org.keycloak.protocol.cas.CASLoginProtocol; |
| | | import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper; |
| | | import org.keycloak.provider.ProviderConfigProperty; |
| | |
| | | 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 { |
| | |
| | | return "Maps the user's first and last name to the OpenID Connect 'name' claim. Format is <first> + ' ' + <last>"; |
| | | } |
| | | |
| | | @Override |
| | | public void setAttribute(Map<String, Object> attributes, ProtocolMapperModel mappingModel, UserSessionModel userSession) { |
| | | UserModel user = userSession.getUser(); |
| | | String protocolClaim = mappingModel.getConfig().get(TOKEN_CLAIM_NAME); |
| | | if (protocolClaim == null) { |
| | | return; |
| | | } |
| | | String first = user.getFirstName() == null ? "" : user.getFirstName() + " "; |
| | | String last = user.getLastName() == null ? "" : user.getLastName(); |
| | | attributes.put(protocolClaim, first + last); |
| | | } |
| | | |
| | | public static ProtocolMapperModel create(String name, String tokenClaimName, |
| | | boolean consentRequired, String consentText) { |
| | | ProtocolMapperModel mapper = new ProtocolMapperModel(); |
| | |
| | | package org.keycloak.protocol.cas.mappers; |
| | | |
| | | import org.keycloak.models.GroupModel; |
| | | import org.keycloak.models.ProtocolMapperModel; |
| | | import org.keycloak.models.UserSessionModel; |
| | | import org.keycloak.models.utils.ModelToRepresentation; |
| | | import org.keycloak.protocol.cas.CASLoginProtocol; |
| | | import org.keycloak.protocol.oidc.OIDCLoginProtocol; |
| | | import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper; |
| | | import org.keycloak.provider.ProviderConfigProperty; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.HashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.*; |
| | | |
| | | public class GroupMembershipMapper extends AbstractCASProtocolMapper { |
| | | private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>(); |
| | |
| | | return "Map user group membership"; |
| | | } |
| | | |
| | | @Override |
| | | public void setAttribute(Map<String, Object> attributes, ProtocolMapperModel mappingModel, UserSessionModel userSession) { |
| | | List<String> membership = new LinkedList<>(); |
| | | boolean fullPath = useFullPath(mappingModel); |
| | | for (GroupModel group : userSession.getUser().getGroups()) { |
| | | if (fullPath) { |
| | | membership.add(ModelToRepresentation.buildGroupPath(group)); |
| | | } else { |
| | | membership.add(group.getName()); |
| | | } |
| | | } |
| | | String protocolClaim = mappingModel.getConfig().get(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME); |
| | | |
| | | attributes.put(protocolClaim, membership); |
| | | } |
| | | |
| | | public static boolean useFullPath(ProtocolMapperModel mappingModel) { |
| | | return "true".equals(mappingModel.getConfig().get("full.path")); |
| | | } |
| | |
| | | package org.keycloak.protocol.cas.mappers; |
| | | |
| | | import org.keycloak.models.ProtocolMapperModel; |
| | | import org.keycloak.models.UserSessionModel; |
| | | import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper; |
| | | import org.keycloak.provider.ProviderConfigProperty; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | import static org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME; |
| | | |
| | | public class HardcodedClaim extends AbstractCASProtocolMapper { |
| | | private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>(); |
| | |
| | | return "Hardcode a claim into the token."; |
| | | } |
| | | |
| | | @Override |
| | | public void setAttribute(Map<String, Object> attributes, ProtocolMapperModel mappingModel, UserSessionModel userSession) { |
| | | String protocolClaim = mappingModel.getConfig().get(TOKEN_CLAIM_NAME); |
| | | if (protocolClaim == null) { |
| | | return; |
| | | } |
| | | String attributeValue = mappingModel.getConfig().get(CLAIM_VALUE); |
| | | if (attributeValue == null) return; |
| | | attributes.put(protocolClaim, OIDCAttributeMapperHelper.mapAttributeValue(mappingModel, attributeValue)); |
| | | } |
| | | |
| | | } |
| | |
| | | package org.keycloak.protocol.cas.mappers; |
| | | |
| | | import org.keycloak.models.ProtocolMapperModel; |
| | | import org.keycloak.models.UserModel; |
| | | import org.keycloak.models.UserSessionModel; |
| | | import org.keycloak.models.utils.KeycloakModelUtils; |
| | | import org.keycloak.protocol.ProtocolMapperUtils; |
| | | import org.keycloak.protocol.cas.CASLoginProtocol; |
| | | import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper; |
| | |
| | | return "Map a custom user attribute to a token claim."; |
| | | } |
| | | |
| | | @Override |
| | | public void setAttribute(Map<String, Object> attributes, ProtocolMapperModel mappingModel, UserSessionModel userSession) { |
| | | UserModel user = userSession.getUser(); |
| | | String protocolClaim = mappingModel.getConfig().get(TOKEN_CLAIM_NAME); |
| | | if (protocolClaim == null) { |
| | | return; |
| | | } |
| | | String attributeName = mappingModel.getConfig().get(ProtocolMapperUtils.USER_ATTRIBUTE); |
| | | List<String> attributeValue = KeycloakModelUtils.resolveAttribute(user, attributeName); |
| | | if (attributeValue == null) return; |
| | | attributes.put(protocolClaim, OIDCAttributeMapperHelper.mapAttributeValue(mappingModel, attributeValue)); |
| | | } |
| | | |
| | | public static ProtocolMapperModel create(String name, String userAttribute, |
| | | String tokenClaimName, String claimType, |
| | | boolean consentRequired, String consentText, boolean multivalued) { |
| | |
| | | package org.keycloak.protocol.cas.mappers; |
| | | |
| | | import org.keycloak.models.ProtocolMapperModel; |
| | | import org.keycloak.models.UserModel; |
| | | import org.keycloak.models.UserSessionModel; |
| | | import org.keycloak.protocol.ProtocolMapperUtils; |
| | | import org.keycloak.protocol.cas.CASLoginProtocol; |
| | | import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper; |
| | |
| | | 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) { |
New file |
| | |
| | | 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; |
| | | } |
| | | } |
New file |
| | |
| | | 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; |
| | | } |
| | | } |
New file |
| | |
| | | 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; |
| | | } |
| | | } |
New file |
| | |
| | | @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; |
New file |
| | |
| | | package org.keycloak.protocol.cas.utils; |
| | | |
| | | import org.keycloak.protocol.cas.representations.CasServiceResponse; |
| | | |
| | | import javax.xml.bind.JAXBElement; |
| | | import javax.xml.bind.annotation.XmlAccessType; |
| | | import javax.xml.bind.annotation.XmlAccessorType; |
| | | import javax.xml.bind.annotation.XmlAnyElement; |
| | | import javax.xml.bind.annotation.XmlSchema; |
| | | import javax.xml.bind.annotation.adapters.XmlAdapter; |
| | | import javax.xml.namespace.QName; |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * Transforms the attribute map of the AuthenticationSuccess object (which can contain either simple values or |
| | | * lists) to a flat list of XML nodes, where the key is the node name.<br> |
| | | * Lists output multiple XML nodes with the same name. |
| | | */ |
| | | public final class AttributesMapAdapter extends XmlAdapter<AttributesMapAdapter.AttributeWrapperType, Map<String, Object>> { |
| | | @Override |
| | | public AttributeWrapperType marshal(Map<String, Object> v) throws Exception { |
| | | return new AttributeWrapperType(v); |
| | | } |
| | | |
| | | @Override |
| | | public Map<String, Object> unmarshal(AttributeWrapperType v) throws Exception { |
| | | throw new IllegalStateException("not implemented"); |
| | | } |
| | | |
| | | @XmlAccessorType(XmlAccessType.FIELD) |
| | | static class AttributeWrapperType { |
| | | @XmlAnyElement |
| | | private final List<JAXBElement<String>> elements; |
| | | |
| | | AttributeWrapperType(Map<String, Object> attributes) { |
| | | this.elements = new ArrayList<>(); |
| | | for (Map.Entry<String, Object> entry : attributes.entrySet()) { |
| | | if (entry.getValue() instanceof List) { |
| | | for (Object item : ((List) entry.getValue())) { |
| | | addElement(entry.getKey(), item); |
| | | } |
| | | } else { |
| | | addElement(entry.getKey(), entry.getValue()); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void addElement(String name, Object value) { |
| | | if (value != null) { |
| | | String namespace = CasServiceResponse.class.getPackage().getAnnotation(XmlSchema.class).namespace(); |
| | | elements.add(new JAXBElement<>(new QName(namespace, name), String.class, value.toString())); |
| | | } |
| | | } |
| | | } |
| | | } |
New file |
| | |
| | | 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(); |
| | | } |
| | | } |
New file |
| | |
| | | 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(); |
| | | } |
| | | } |
| | | } |
New file |
| | |
| | | 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); |
| | | } |
| | | } |
| | | } |
New file |
| | |
| | | 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)); |
| | | } |
| | | } |