Share CAS logout

Update – since surf 1.2 (at least) the Surf LogoutController understands the redirectURL request parameter (and redirectURLQueryKey, redirectURLQueryValue) which makes this all unnecessary – add the parameters to the end of the dologout call in the header – how depends on the version of Share – in share-config-custom.xml for the old style header – see  here for the new style header

I’ve finally had the time to work out how to configure Alfresco Share to successfully log out when using CAS to log in.

If you log out of just CAS you will still be logged into Share and vice versa so you need to log out of both – and just for good measure log out of Alfresco at the same time.

This follows on from Martin Bergljung’s blog on configuring for CAS

(Note this doesn’t do anything about single sign out as that’s a different problem)

The way I’ve done this is to override the logoutController by defined a new java class and referencing it in custom-slingshot-application-context.xml

 <!-- Override Logout Controller - to expire Alfresco tickets -->
   <bean id="logoutController" class="org.wrighting.web.site.servlet.CASSlingshotLogoutController">
      <property name="cacheSeconds" value="-1" />
      <property name="useExpiresHeader"><value>true</value></property>
      <property name="useCacheControlHeader"><value>true</value></property>
      <property name="connectorService" ref="connector.service" />
      <!-- if blank assumes the same as Share -->

      <property name="casHost"><value>https://alfresco</value></property>
      <property name="casPath"><value>sso/logout</value></property>

   </bean>

You’ll see here that I’ve defined a couple to properties to indicate where the CAS log out page lies – this is where the user will be redirected when they log out. This fits quite closely with the method of configuring the CAS server in the web.xml but you could use an alternative approach such as properties.

The java code is very closely based on org.alfresco.web.site.servlet.SlingshotLogoutController however there is one significant difference in that it doesn’t call handleRequestInternal in LogoutController.
This is because LogoutController.handleRequestInternal ends up redirecting to request.getContextPath whereas we want to go to our CAS logout page however it’s still necessary to make sure that you call AuthenticationUtil.logout which would normally be done in the LogoutController.

package org.wrighting.web.site.servlet;

import java.net.URL;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.extensions.surf.UserFactory;
import org.springframework.extensions.surf.mvc.LogoutController;
import org.springframework.extensions.surf.site.AuthenticationUtil;
import org.springframework.extensions.surf.support.AlfrescoUserFactory;
import org.springframework.extensions.webscripts.connector.AlfrescoAuthenticator;
import org.springframework.extensions.webscripts.connector.Connector;
import org.springframework.extensions.webscripts.connector.ConnectorContext;
import org.springframework.extensions.webscripts.connector.ConnectorService;
import org.springframework.extensions.webscripts.connector.HttpMethod;
import org.springframework.extensions.webscripts.connector.Response;
import org.springframework.web.servlet.ModelAndView;

/**
 * CAS specific override of the Share specific override of the SpringSurf
 * dologout controller.
 * <p>
 * The implementation ensures Alfresco tickets are removed if appropriate and as
 * it can't delegates to the SpringSurf implementation for framework cleanup
 * does that clean up and then sends a logout to the CAS host
 *
 * @see org.alfresco.web.site.servlet.SlingshotLogoutController
 *
 * @author Ian Wright
 */
public class CASSlingshotLogoutController extends LogoutController {
    private static Log logger = LogFactory
            .getLog(CASSlingshotLogoutController.class);
    private ConnectorService connectorService;
    private String casHost;
    private String casPath;

    /**
     * @param connectorService
     *            the ConnectorService to set
     */
    public void setConnectorService(ConnectorService connectorService) {
        this.connectorService = connectorService;
    }

    @Override
    public ModelAndView handleRequestInternal(HttpServletRequest request,
            HttpServletResponse response) throws Exception {

        try {
            HttpSession session = request.getSession(false);
            if (session != null) {
                // retrieve the current user ID from the session
                String userId = (String) session
                        .getAttribute(UserFactory.SESSION_ATTRIBUTE_KEY_USER_ID);

                if (userId != null) {
                    // get the ticket from the Alfresco connector
                    Connector connector = connectorService.getConnector(
                            AlfrescoUserFactory.ALFRESCO_ENDPOINT_ID, userId,
                            session);
                    String ticket = connector.getConnectorSession()
                            .getParameter(
                                    AlfrescoAuthenticator.CS_PARAM_ALF_TICKET);

                    if (ticket != null) {
                        // if we found a ticket, then expire it via REST API -
                        // not all auth will have a ticket i.e. SSO
                        Response res = connector.call("/api/login/ticket/"
                                + ticket, new ConnectorContext(
                                HttpMethod.DELETE));
                        if (logger.isDebugEnabled())
                            logger.debug("Expired ticket: " + ticket
                                    + " user: " + userId + " - status: "
                                    + res.getStatus().getCode());
                    }
                }
            }
        } finally {
            AuthenticationUtil.logout(request, response);
            String target = request.getContextPath();
            if (casHost != null && casHost.length() > 0) {
                target = casHost;
            } else {
                URL reconstructedURL = new URL(request.getScheme(),
                        request.getServerName(),
                        request.getServerPort(),
                        "");
                target = reconstructedURL.toExternalForm();
            }
            if (casPath != null && casPath.length() > 0) {
                target += '/' + casPath;
            }
            if (logger.isDebugEnabled()) {
                logger.debug("Logout to:" + target);
            }
            response.sendRedirect(target);
        }

        return null;
    }

    /**
     * @param casHostValue
     *            the casHost to set - defaults to the same as Share
     */
    public void setCasHost(String casHostValue) {
        casHost = casHostValue;
    }

    /**
     * @param casPathValue
     *            the location of the CAS logout servlet
     */
    public void setCasPath(String casPathValue) {
        casPath = casPathValue;
    }
}