org.everit.osgi.authentication.cas.internal.CasAuthenticationComponent.java Source code

Java tutorial

Introduction

Here is the source code for org.everit.osgi.authentication.cas.internal.CasAuthenticationComponent.java

Source

/**
 * This file is part of Everit - CAS authentication.
 *
 * Everit - CAS authentication is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Everit - CAS authentication is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Everit - CAS authentication.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.everit.osgi.authentication.cas.internal;

import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Map;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionAttributeListener;
import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import javax.xml.parsers.SAXParserFactory;

import org.apache.commons.io.IOUtils;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.ConfigurationPolicy;
import org.apache.felix.scr.annotations.Properties;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;
import org.everit.osgi.authentication.cas.CasAuthenticationConstants;
import org.everit.osgi.authentication.http.session.AuthenticationSessionAttributeNames;
import org.everit.osgi.resource.resolver.ResourceIdResolver;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.service.cm.ConfigurationException;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.log.LogService;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;

/**
 * A component that supports CAS authentication. The following cases are handled by this component:
 * <ul>
 * <li><b>CAS service ticket validation</b>: If the request contains a CAS service ticket, it will be validated on the
 * CAS server by invoking its service ticket validation URL. If the ticket is valid and the returned principal
 * (username) can be mapped to a Resource ID, then the Resource ID will be assigned to the session.</li>
 * <li><b>CAS logout request processing</b>: If the request is a CAS logout request, then the session assigned to the
 * service ticket (received in the logout request) will be invalidated. The CAS server sends the logout request
 * asynchronously to the clients, therefore the session of the logout request is not the same as the session of the
 * user. The mapping of service tickets and sessions are handled by the {@link CasHttpSessionRegistry}.</li>
 * </ul>
 * <p>
 * <b>Implemented interfaces</b>
 * </p>
 * <ul>
 * <li><b>{@link Filter}</b>: Handles the CAS service ticket validation and CAS logout request processing.</li>
 * <li><b>{@link ServletContextListener}</b>: Registers and removes the {@link CasHttpSessionRegistry} to and from the
 * {@link ServletContext}.</li>
 * <li><b>{@link HttpSessionListener}</b>: Clears the {@link CasHttpSessionRegistry} when a {@link HttpSession} is
 * invalidated.</li>
 * <li><b>{@link HttpSessionAttributeListener}</b>: Registers and removes the {@link CasHttpSessionActivationListener}
 * to and from the {@link HttpSession} because it IS NOT {@link java.io.Serializable} and cannot be
 * instantiated/deserialized by a non-OSGi technology. Class loading problems can occur when deserializing this
 * {@link java.util.EventListener} if it is still in the {@link HttpSession} during serialization.</li>
 * </ul>
 * <p>
 * It is recommended to use this component in pair with <a
 * href="https://github.com/everit-org/authentication-http-session">authentication-http-session</a>
 * </p>
 */
@Component(name = CasAuthenticationConstants.SERVICE_FACTORYPID_CAS_AUTHENTICATION, metatype = true, configurationFactory = true, policy = ConfigurationPolicy.REQUIRE, immediate = true)
@Properties({
        @Property(name = Constants.SERVICE_DESCRIPTION, propertyPrivate = false, value = CasAuthenticationConstants.DEFAULT_SERVICE_DESCRIPTION_CAS_AUTHENTICATION),
        @Property(name = CasAuthenticationConstants.PROP_CAS_SERVICE_TICKET_VALIDATION_URL, value = CasAuthenticationConstants.DEFAULT_CAS_SERVICE_TICKET_VALIDATION_URL),
        @Property(name = CasAuthenticationConstants.PROP_FAILURE_URL, value = CasAuthenticationConstants.DEFAULT_FAILURE_URL),
        @Property(name = CasAuthenticationConstants.PROP_AUTHENTICATION_SESSION_ATTRIBUTE_NAMES),
        @Property(name = CasAuthenticationConstants.PROP_RESOURCE_ID_RESOLVER),
        @Property(name = CasAuthenticationConstants.PROP_SAX_PARSER_FACTORY),
        @Property(name = CasAuthenticationConstants.PROP_LOG_SERVICE), })
@Service
public class CasAuthenticationComponent
        implements Filter, ServletContextListener, HttpSessionListener, HttpSessionAttributeListener {

    /**
     * The template of the CAS service ticket validation URL. Parameters in order:
     * <ul>
     * <li>1: CAS server service ticket validation URL, for e.g.: https://mycas.com/cas/serviceValidate</li>
     * <li>2: The URL encoded service URL, for e.g.: http://myapp.com/hello?foo=bar. The user will be redirected to this
     * URL after a successful validation.</li>
     * <li>3: The service ticket sent by the CAS server.</li>
     * </ul>
     */
    private static final String SERVICE_TICKET_VALIDATOR_URL_TEMPLATE = "%1$s?service=%2$s&ticket=%3$s";

    /**
     * The element name in logout request sent by the CAS server that contains the invalidated service ticket.
     */
    private static final String SESSION_INDEX = "SessionIndex";

    /**
     * The element name in the successful service ticket validation response sent by the CAS server that contains the
     * name/principal of the authenticated user.
     */
    private static final String USER = "user";

    /**
     * The element name in the failed service ticket validation response sent by the CAS server that contains the
     * message why the authentication failed.
     */
    private static final String AUTHENTICATION_FAILURE = "authenticationFailure";

    /**
     * The request parameter name used to specify the locale of the user when communicating with the CAS server.
     */
    private static final String LOCALE = "locale";

    /**
     * The default value of the {@link #requestParamNameServiceTicket}.
     */
    private static final String DEFAULT_REQ_PARAM_NAME_SERVICE_TICKET = "ticket";

    /**
     * The default value of the {@link #requestParamNameLogoutRequest}.
     */
    private static final String DEFAULT_REQ_PARAM_NAME_LOGOUT_REQUEST = "logoutRequest";

    @Reference(bind = "setAuthenticationSessionAttributeNames")
    private AuthenticationSessionAttributeNames authenticationSessionAttributeNames;

    @Reference(bind = "setResourceIdResolver")
    private ResourceIdResolver resourceIdResolver;

    @Reference(bind = "setSaxParserFactory")
    private SAXParserFactory saxParserFactory;

    @Reference(bind = "setLogService")
    private LogService logService;

    /**
     * The service ticket validation URL of the CAS server.
     */
    private String casServiceTicketValidatorUrl;

    /**
     * The URL where the user will be redirected in case of failures.
     */
    private String failureUrl;

    /**
     * The HTTP request parameter name used by the CAS server when it sends the Service Ticket (ST) for validation to
     * the protected application.
     */
    private String requestParamNameServiceTicket = DEFAULT_REQ_PARAM_NAME_SERVICE_TICKET;

    /**
     * The HTTP request parameter name used by the CAS server when a Service Ticket (ST) is invalidated due to logout.
     */
    private String requestParamNameLogoutRequest = DEFAULT_REQ_PARAM_NAME_LOGOUT_REQUEST;

    /**
     * The persistent identifier of the services.
     */
    private String servicePid;

    /**
     * {@link org.apache.felix.scr.annotations.Activate} method of the component. It collects the configured component
     * properties and stores it in the member variables.
     */
    @Activate
    public void activate(final BundleContext context, final Map<String, Object> componentProperties)
            throws Exception {
        casServiceTicketValidatorUrl = getStringProperty(componentProperties,
                CasAuthenticationConstants.PROP_CAS_SERVICE_TICKET_VALIDATION_URL);
        failureUrl = getStringProperty(componentProperties, CasAuthenticationConstants.PROP_FAILURE_URL);
        servicePid = getStringProperty(componentProperties, Constants.SERVICE_PID);
    }

    /**
     * Handles the case when a special session attribute is added. If an attribute added (manually or when restoring a
     * persistent session) with name starting with
     * {@link CasHttpSessionActivationListener#SESSION_ATTR_NAME_SERVICE_PID_PREFIX} this listener method will:
     * <ul>
     * <li>Register a {@link CasHttpSessionActivationListener} instance to the session and remove the added session
     * attribute if the {@link CasHttpSessionActivationListener} IS NOT REGISTERED to the session already with the
     * Service PID stored in the session. This is necessary to re-register the EventListener when a session is restored
     * from its persistent state.</li>
     * <li>Remove the {@link CasHttpSessionActivationListener} instance from the session if the
     * {@link CasHttpSessionActivationListener} IS REGISTERED to the session already with the Service PID stored in the
     * session. This is necessary to remove the EventListener from the session before it will be Serialized, because the
     * {@link CasHttpSessionActivationListener} is not {@link java.io.Serializable} and cannot be
     * instantiated/deserialized by a non-OSGi technology</li>
     * </ul>
     */
    @Override
    public void attributeAdded(final HttpSessionBindingEvent event) {
        String addedAttributeName = event.getName();
        if (addedAttributeName.startsWith(CasHttpSessionActivationListener.SESSION_ATTR_NAME_SERVICE_PID_PREFIX)) {

            String servicePid = (String) event.getValue();
            String casHttpSessionActivationListenerSessionAttrName = CasHttpSessionActivationListener
                    .createSessionAttrNameInstance(servicePid);

            HttpSession httpSession = event.getSession();
            if (httpSession.getAttribute(casHttpSessionActivationListenerSessionAttrName) == null) {

                CasHttpSessionActivationListener.registerInstance(servicePid, httpSession);
                String attributeNameToRemove = CasHttpSessionActivationListener
                        .createSessionAttrNameServicePid(servicePid);
                if (attributeNameToRemove.equals(addedAttributeName)) {
                    httpSession.removeAttribute(attributeNameToRemove);
                }
            } else {
                CasHttpSessionActivationListener.removeInstance(servicePid, httpSession);
            }
        }
    }

    @Override
    public void attributeRemoved(final HttpSessionBindingEvent event) {
        // Nothing to do
    }

    @Override
    public void attributeReplaced(final HttpSessionBindingEvent event) {
        // Nothing to do
    }

    /**
     * Removes the previously registered {@link CasHttpSessionRegistry} from the {@link ServletContext} when it is
     * destroyed.
     */
    @Override
    public void contextDestroyed(final ServletContextEvent servletContextEvent) {
        ServletContext servletContext = servletContextEvent.getServletContext();
        CasHttpSessionRegistry.removeInstance(servicePid, servletContext);
    }

    /**
     * Registers the {@link CasHttpSessionRegistry} to the {@link ServletContext} when it is initialized.
     */
    @Override
    public void contextInitialized(final ServletContextEvent servletContextEvent) {
        ServletContext servletContext = servletContextEvent.getServletContext();
        CasHttpSessionRegistry.registerInstance(servicePid, servletContext);
    }

    /**
     * Creates the service URL forwarded to the CAS server as "service" parameter in case of service ticket validation.
     * This URL will be used to redirect the user if the service ticket validation succeeds.
     *
     * @param httpServletRequest
     *            the request used to build the service URL
     * @return the service URL
     */
    private String createServiceUrl(final HttpServletRequest httpServletRequest) {
        String queryString = httpServletRequest.getQueryString();
        if (queryString == null) {
            queryString = "";
        } else {
            int serviceTicketLocation = queryString.indexOf(requestParamNameServiceTicket);
            if (serviceTicketLocation <= 0) {
                queryString = "";
            } else {
                queryString = queryString.substring(0, queryString.indexOf("&" + requestParamNameServiceTicket));
                queryString = "?" + queryString;
            }
        }
        String serviceUrl = httpServletRequest.getRequestURL().append(queryString).toString();
        return serviceUrl;
    }

    @Override
    public void destroy() {
        // Nothing to do here.
    }

    /**
     * The method that processes the request is filter's url pattern matches the requst. This method handles one of the
     * followings in order:
     * <ul>
     * <li>Performs a CAS service ticket validation if the request contains a service ticket named by
     * {@link #requestParamNameServiceTicket}.</li>
     * <li>Processes a back channel logout initiated by the CAS server if the request is POST request and contains a
     * parameter named by {@link #requestParamNameLogoutRequest}.</li>
     * <li>Invokes further the {@link FilterChain}.</li>
     * </ul>
     */
    @Override
    public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest httpServletRequest = (HttpServletRequest) request;

        String serviceTicket = getRequestParameter(httpServletRequest, requestParamNameServiceTicket);

        if (serviceTicket != null) {
            HttpServletResponse httpServletResponse = (HttpServletResponse) response;
            performServiceTicketValidation(httpServletRequest, httpServletResponse, serviceTicket);

        } else if (isCasLogoutRequest(httpServletRequest)) {
            HttpServletResponse httpServletResponse = (HttpServletResponse) response;
            processBackChannelLogout(httpServletRequest, httpServletResponse);

        } else {
            // Go further in the filter chain if the request does not contain the service ticket neither a back channel
            // logout request
            chain.doFilter(request, response);
        }

    }

    /**
     * Returns the value of a request parameter if available.
     *
     * @param httpServletRequest
     *            the request to check for the parameter.
     * @param name
     *            the name of the parameter to check
     * @return the value of the requested parameter if available and not empty, otherwise <code>null</code>
     */
    private String getRequestParameter(final HttpServletRequest httpServletRequest, final String name) {
        if (!isRequestContains(httpServletRequest, name)) {
            return null;
        }
        String ticket = httpServletRequest.getParameter(name);
        if (ticket == null) {
            return null;
        }
        ticket = ticket.trim();
        if (ticket.isEmpty()) {
            return null;
        }
        return ticket;
    }

    private String getStringProperty(final Map<String, Object> componentProperties, final String propertyName)
            throws ConfigurationException {
        Object value = componentProperties.get(propertyName);
        if (value == null) {
            throw new ConfigurationException(propertyName, "property not defined");
        }
        return String.valueOf(value);
    }

    /**
     * Returns the value of an XML element. This method is used to process the XMLs sent by the CAS server.
     *
     * @param xmlAsString
     *            the XML string to process
     * @param elementName
     *            the name of the queried element
     * @return the value assigned to the queried element name
     * @throws RuntimeException
     *             if any error occurs during the parsing of the XML string
     */
    private String getTextForElement(final String xmlAsString, final String elementName) {

        XMLReader xmlReader;
        try {
            xmlReader = saxParserFactory.newSAXParser().getXMLReader();
            xmlReader.setFeature("http://xml.org/sax/features/namespaces", true);
            xmlReader.setFeature("http://xml.org/sax/features/namespace-prefixes", false);
            xmlReader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
        } catch (final Exception e) {
            throw new RuntimeException("Unable to create XMLReader", e);
        }

        StringBuilder builder = new StringBuilder();

        DefaultHandler handler = new DefaultHandler() {

            private boolean foundElement = false;

            @Override
            public void characters(final char[] ch, final int start, final int length) throws SAXException {
                if (foundElement) {
                    builder.append(ch, start, length);
                }
            }

            @Override
            public void endElement(final String uri, final String localName, final String qName)
                    throws SAXException {
                if (localName.equals(elementName)) {
                    foundElement = false;
                }
            }

            @Override
            public void startElement(final String uri, final String localName, final String qName,
                    final Attributes attributes) throws SAXException {
                if (localName.equals(elementName)) {
                    foundElement = true;
                }
            }
        };

        xmlReader.setContentHandler(handler);
        xmlReader.setErrorHandler(handler);

        try {
            xmlReader.parse(new InputSource(new StringReader(xmlAsString)));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        return builder.toString();
    }

    /**
     * Redirects the response to the configured {@link #failureUrl} and logs the given message and exception.
     *
     * @param httpServletResponse
     *            the response used to redirect
     * @param message
     *            the error message to log
     * @param e
     *            the exception to log
     * @throws IOException
     *             if an input or output exception occurs
     */
    private void handleError(final HttpServletResponse httpServletResponse, final String message, final Exception e)
            throws IOException {
        logService.log(LogService.LOG_ERROR, message, e);
        if (failureUrl != null) {
            httpServletResponse.sendRedirect(failureUrl);
        } else {
            httpServletResponse.sendError(HttpServletResponse.SC_FORBIDDEN, message);
        }
    }

    @Override
    public void init(final FilterConfig filterConfig) throws ServletException {
        // Nothing to do here.
    }

    /**
     * Checks if the request is a CAS logout request.
     *
     * @param httpServletRequest
     *            the request to check
     * @return <code>true</code> if the request is a POST and a parameter with key
     *         {@link #requestParamNameLogoutRequest} exists in the request, otherwise <code>false</code>
     */
    private boolean isCasLogoutRequest(final HttpServletRequest httpServletRequest) {
        return httpServletRequest.getMethod().equals("POST")
                && httpServletRequest.getParameterMap().containsKey(requestParamNameLogoutRequest);
    }

    /**
     * Checks if the query string of the request contains the given name.
     *
     * @param httpServletRequest
     *            the request to check
     * @param name
     *            the name to check with
     * @return <code>true</code> if the query string is not <code>null</code> and contains the <code>name</code>
     *         argument, otherwise <code>false</code>
     */
    private boolean isRequestContains(final HttpServletRequest httpServletRequest, final String name) {
        String queryString = httpServletRequest.getQueryString();
        return (queryString != null) && queryString.contains(name);
    }

    /**
     * Performs a CAS service ticket validation. The following tasks are done if the service ticket is valid:
     * <ul>
     * <li>The authenticated username/principal sent by the CAS server is mapped to a Resource ID.</li>
     * <li>The mapped Resource ID is added to the {@link HttpSession} with the name provided by the
     * {@link AuthenticationSessionAttributeNames#authenticatedResourceId()} method. This Resource ID will be picked up
     * by the {@link Filter} provided by the <a
     * href="https://github.com/everit-org/authentication-http-session">authentication-http-session</a> component and
     * that filter will execute the authenticated process in the name of the authenticated user.</li>
     * <li>A {@link CasHttpSessionActivationListener} is also registered to the session to handle session passivation
     * and activation events (for e.g. in case of persistent sessions).</li>
     * <li>The {@link HttpSession} is registered to the {@link CasHttpSessionRegistry} stored in the
     * {@link ServletContext} to be able to handle CAS logout requests (invalidate the proper session belonging to the
     * service ticket).</li>
     * <li>Redirects the response to the service URL.</li>
     * </ul>
     */
    private void performServiceTicketValidation(final HttpServletRequest httpServletRequest,
            final HttpServletResponse httpServletResponse, final String serviceTicket) throws IOException {

        String serviceUrl = createServiceUrl(httpServletRequest);
        String locale = getRequestParameter(httpServletRequest, LOCALE);

        try {
            String principal = validateServiceTicket(serviceUrl, serviceTicket, locale);

            Long authenticatedResourceId = resourceIdResolver.getResourceId(principal)
                    .orElseThrow(() -> new IllegalStateException("The principal [" + principal
                            + "] of the valid service ticket cannot be mapped to a Resource ID."
                            + " The session will not be assigned to any Resource ID."));

            HttpSession httpSession = httpServletRequest.getSession();
            httpSession.setAttribute(authenticationSessionAttributeNames.authenticatedResourceId(),
                    authenticatedResourceId);

            CasHttpSessionActivationListener.registerInstance(servicePid, httpSession);

            ServletContext servletContext = httpServletRequest.getServletContext();
            CasHttpSessionRegistry casHttpSessionRegistry = CasHttpSessionRegistry.getInstance(servicePid,
                    servletContext);
            casHttpSessionRegistry.put(serviceTicket, httpSession);

            httpServletResponse.sendRedirect(serviceUrl);

        } catch (IllegalStateException | TicketValidationException e) {
            handleError(httpServletResponse, e.getMessage(), e);
        }
    }

    /**
     * Processes the CAS (back channel) logout requests. It retrieves the invalidated service ticket from the logout
     * request and invalidates the {@link HttpSession} assigned to that service ticket.
     */
    private void processBackChannelLogout(final HttpServletRequest httpServletRequest,
            final HttpServletResponse httpServletResponse) throws IOException {

        String logoutRequest = httpServletRequest.getParameter(requestParamNameLogoutRequest);
        String sessionIndex = getTextForElement(logoutRequest, SESSION_INDEX);

        ServletContext servletContext = httpServletRequest.getServletContext();
        CasHttpSessionRegistry casHttpSessionRegistry = CasHttpSessionRegistry.getInstance(servicePid,
                servletContext);

        casHttpSessionRegistry.removeByServiceTicket(sessionIndex).ifPresent((httpSession) -> {
            try {
                httpSession.invalidate();
            } catch (IllegalStateException e) {
                logService.log(LogService.LOG_DEBUG, e.getMessage(), e);
            }
        });
    }

    @Override
    public void sessionCreated(final HttpSessionEvent httpSessionEvent) {
        // Nothing to do here.
    }

    /**
     * When an {@link HttpSession} is destroyed it must be removed from the {@link CasHttpSessionRegistry}.
     */
    @Override
    public void sessionDestroyed(final HttpSessionEvent httpSessionEvent) {
        HttpSession httpSession = httpSessionEvent.getSession();
        ServletContext servletContext = httpSession.getServletContext();

        CasHttpSessionRegistry casHttpSessionRegistry = CasHttpSessionRegistry.getInstance(servicePid,
                servletContext);
        casHttpSessionRegistry.removeBySession(httpSession);
    }

    public void setAuthenticationSessionAttributeNames(
            final AuthenticationSessionAttributeNames authenticationSessionAttributeNames) {
        this.authenticationSessionAttributeNames = authenticationSessionAttributeNames;
    }

    public void setLogService(final LogService logService) {
        this.logService = logService;
    }

    public void setResourceIdResolver(final ResourceIdResolver resourceIdResolver) {
        this.resourceIdResolver = resourceIdResolver;
    }

    public void setSaxParserFactory(final SAXParserFactory saxParserFactory) {
        this.saxParserFactory = saxParserFactory;
    }

    /**
     * Validates a CAS service ticket and returns the username/principal belonging to that ticket.
     *
     * @param serviceUrl
     *            the service URL used to validate the service ticket
     * @param serviceTicket
     *            the service ticket to validate
     * @param locale
     *            the locale of the user used in the communication with the CAS server
     * @return the authenticated (in case of valid service ticket) username/principal
     * @throws IOException
     *             if an input or output exception occurs
     * @throws TicketValidationException
     *             if the ticket validation fails
     */
    private String validateServiceTicket(final String serviceUrl, final String serviceTicket, final String locale)
            throws IOException, TicketValidationException {

        String validationUrl = String.format(SERVICE_TICKET_VALIDATOR_URL_TEMPLATE, casServiceTicketValidatorUrl,
                URLEncoder.encode(serviceUrl, StandardCharsets.UTF_8.displayName()), serviceTicket);
        if (locale != null) {
            validationUrl = validationUrl + "&" + LOCALE + "=" + locale;
        }

        URL url = new URL(validationUrl);
        try (InputStream inputStream = url.openStream()) {
            StringWriter writer = new StringWriter();
            IOUtils.copy(inputStream, writer);
            String response = writer.toString();
            String error = getTextForElement(response, AUTHENTICATION_FAILURE);
            if ((error != null) && !error.trim().isEmpty()) {
                throw new TicketValidationException(error.trim());
            }
            return getTextForElement(response, USER);
        }

    }

}