/* vim: set ts=2 et sw=2 cindent fo=qroca: */ package com.globant.katari.cas; import java.io.IOException; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; 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 org.apache.commons.lang.Validate; import org.springframework.util.Assert; /** Filter that logs out a user using CAS. * * When the users issues a request to /logout, this filter initiates the cas * logout. Once the logout finishes, this filter redirects to the service * parameter, for example:<br> * * /logout?service=/katari-sample/module/home.do<br> * * redirects the user to the home.do controller after logout. If no service * parameter is found, it redirects to the context path. * * When a user logs out from a service, CAS posts for each server a logout * request at the same url which CAS redirected after the successful login.<br> * * The post is made by the CAS server, so it doesn't belong to the user * sessions. That is the reason why this filter depends on * {@link CasTicketRegistry}, an object that stores which tickets belongs to * what sessions.<br> * * CAS send a logout request with the ticket issued and this filter invalidates * the session that is related to that ticket. * * @author pruggia */ public class CasLogoutFilter implements Filter { /** * Start of a valid logout request, used to know if this filter has to be * activated. */ private static final String LOGOUT_REQUEST_XML_START = "<samlp:LogoutRequest"; /** * Text just before the ticket string in the logout request. */ private static final String LOGOUT_TICKET_START = "<samlp:SessionIndex>"; /** * Text just after the ticket string in the logout request. */ private static final String LOGOUT_TICKET_END = "</samlp:SessionIndex>"; /** Substring that this filter tries to match in order to be active. */ private String filterProcessesUrl = "/j_acegi_cas_security_check"; /** Substrig that indicates the logout path. */ private String filterLogoutUrl = "/logout"; /** Substrig that indicates the cas logout path. */ private String casLogoutUrl = "/module/cas/logout"; /** Registry used to bind CAS tickets with user sessions. */ private CasTicketRegistry casTicketRegistry; /** The CasTicketRegistry constructor. * * @param theCasTicketRegistry The cas ticket registry. It cannot be null. */ public CasLogoutFilter(final CasTicketRegistry theCasTicketRegistry) { Validate.notNull(theCasTicketRegistry); this.casTicketRegistry = theCasTicketRegistry; } /** * {@inheritDoc} */ public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException { if (!(request instanceof HttpServletRequest)) { throw new ServletException("Can only process HttpServletRequest"); } if (!(response instanceof HttpServletResponse)) { throw new ServletException("Can only process HttpServletResponse"); } HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; if (isRequestToUri(httpRequest, filterLogoutUrl)) { String redirectUrl; redirectUrl = httpResponse.encodeRedirectURL(httpRequest.getContextPath() + casLogoutUrl); String service = httpRequest.getParameter("service"); if (service == null) { service = httpRequest.getContextPath(); } httpResponse.sendRedirect(redirectUrl + "?service=" + service); return; } String ticket = getTicketFromRequest(httpRequest); if (ticket != null) { HttpSession session = casTicketRegistry.getSession(ticket); if (session != null) { // TODO The handlers are not called here. Basically the problem is that // the request is not issued by the user, it's made by the cas server, // so the cookies of the user and the session of the user are not // reachable from here. To solve the session problem we can create a // request wrapper that rewrites the getSession() method, but there is // no simple hack for the cookies. // for (int i = 0; i < handlers.length; i++) { // handlers[i].logout(new CasLogoutRequestWrapper(httpRequest, // session), httpResponse, auth); // } session.invalidate(); } return; } chain.doFilter(request, response); } /** * Reads the post searching for a "logoutRequest" parameter. * @param request The {@link HttpServletRequest}. * @return null if the logout request is not a valid one, a String if the * logout request is valid. * @throws IOException If there was any reading error. */ protected String readLogoutRequest(final HttpServletRequest request) throws IOException { // now we check if cas is posting a login or a logout if (!request.getMethod().equals("POST")) { return ""; } return request.getParameter("logoutRequest"); } /** * Extracts the ticket from inside the logout request. * @param request The {@link HttpServletRequest}. * @return A String representing the ticket that belongs to the user that * wants to logout, null if the ticket couldn't be found. * @throws IOException If there was any reading error. */ protected String getTicketFromRequest(final HttpServletRequest request) throws IOException { if (!isRequestToUri(request, filterProcessesUrl)) { return null; } String post = readLogoutRequest(request); if (post.length() < LOGOUT_REQUEST_XML_START.length() || !post.substring(0, LOGOUT_REQUEST_XML_START.length()).equals( LOGOUT_REQUEST_XML_START)) { return null; } int ticketStart = post.indexOf(LOGOUT_TICKET_START); int ticketEnd = post.indexOf(LOGOUT_TICKET_END); if (ticketStart < 0 || ticketEnd < 0) { return null; } return post .substring(ticketStart + LOGOUT_TICKET_START.length(), ticketEnd); } /** * Sets the substring that this filter tries to match in order to be active. * @param theFilterProcessesUrl The url, cannot be null. */ public void setFilterProcessesUrl(final String theFilterProcessesUrl) { Assert.hasText(theFilterProcessesUrl, "FilterProcessesUrl required"); filterProcessesUrl = theFilterProcessesUrl; } /** * {@inheritDoc} */ public void init(final FilterConfig arg0) throws ServletException { } /** * {@inheritDoc} */ public void destroy() { } /** This method indicated if the given request is related to the given url. * * @param request the request. * @param url the url. * @return if the given request is related to the given url. */ protected boolean isRequestToUri(final HttpServletRequest request , final String url) { String uri = request.getRequestURI(); int pathParamIndex = uri.indexOf(';'); if (pathParamIndex > 0) { // strip everything after the first semi-colon uri = uri.substring(0, pathParamIndex); } return uri.endsWith(request.getContextPath() + url); } }