001/*
002 *  (C) Copyright 2000-2003 Yale University. All rights reserved.
003 *
004 *  THIS SOFTWARE IS PROVIDED "AS IS," AND ANY EXPRESS OR IMPLIED
005 *  WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
006 *  MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, ARE EXPRESSLY
007 *  DISCLAIMED. IN NO EVENT SHALL YALE UNIVERSITY OR ITS EMPLOYEES BE
008 *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
009 *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED, THE COSTS OF
010 *  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR
011 *  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
012 *  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
013 *  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
014 *  SOFTWARE, EVEN IF ADVISED IN ADVANCE OF THE POSSIBILITY OF SUCH
015 *  DAMAGE.
016 *
017 *  Redistribution and use of this software in source or binary forms,
018 *  with or without modification, are permitted, provided that the
019 *  following conditions are met:
020 *
021 *  1. Any redistribution must include the above copyright notice and
022 *  disclaimer and this list of conditions in any related documentation
023 *  and, if feasible, in the redistributed software.
024 *
025 *  2. Any redistribution must include the acknowledgment, "This product
026 *  includes software developed by Yale University," in any related
027 *  documentation and, if feasible, in the redistributed software.
028 *
029 *  3. The names "Yale" and "Yale University" must not be used to endorse
030 *  or promote products derived from this software.
031 */
032
033package edu.yale.its.tp.cas.client.filter;
034
035import java.io.*;
036import java.net.*;
037import java.util.*;
038import javax.servlet.*;
039import javax.servlet.http.*;
040import edu.yale.its.tp.cas.client.*;
041import edu.yale.its.tp.cas.client.filter.*;
042import javax.xml.parsers.ParserConfigurationException;
043import org.xml.sax.SAXException;
044
045/**
046 * <p>
047 * Protects web-accessible resources with CAS.
048 * </p>
049 * <p>
050 * The following filter initialization parameters are declared in <code>web.xml</code>:
051 * </p>
052 * <ul>
053 * <li><code>edu.yale.its.tp.cas.client.filter.loginUrl</code>: URL to login page on CAS server. (Required)</li>
054 * <li><code>edu.yale.its.tp.cas.client.filter.validateUrl</code>: URL to validation URL on CAS server. (Required)</li>
055 * <li><code>edu.yale.its.tp.cas.client.filter.serviceUrl</code>: URL of this service. (Required if
056 * <code>serverName</code> is not specified)</li>
057 * <li><code>edu.yale.its.tp.cas.client.filter.serverName</code>: full hostname with port number (e.g.
058 * <code>www.foo.com:8080</code>). Port number isn't required if it is standard (80 for HTTP, 443 for HTTPS). (Required
059 * if <code>serviceUrl</code> is not specified)</li>
060 * <li><code>edu.yale.its.tp.cas.client.filter.authorizedProxy</code>: whitespace-delimited list of valid proxies
061 * through which authentication may have proceeded. One one proxy must match. (Optional. If nothing is specified, the
062 * filter will only accept service tickets &#150; not proxy tickets.)</li>
063 * <li><code>edu.yale.its.tp.cas.client.filter.renew</code>: value of CAS "renew" parameter. Bypasses single sign-on and
064 * requires user to provide CAS with his/her credentials again. (Optional. If nothing is specified, this defaults to
065 * false.)</li>
066 * <li><code>edu.yale.its.tp.cas.client.filter.wrapRequest</code>: wrap the <code>HttpServletRequest</code> object,
067 * overriding the <code>getRemoteUser()</code> method. When set to "true", <code>request.getRemoteUser()</code> will
068 * return the username of the currently logged-in CAS user. (Optional. If nothing is specified, this defaults to false.)
069 * </li>
070 * </ul>
071 * <p>
072 * The logged-in username is set in the session attribute defined by the value of <code>CAS_FILTER_USER</code> and may
073 * be accessed from within your application either by setting <code>wrapRequest</code> and calling
074 * <code>request.getRemoteUser()</code>, or by calling <code>session.getAttribute(CASFilter.CAS_FILTER_USER)</code>.
075 * </p>
076 *
077 * @author Shawn Bayern
078 */
079public class CASFilter implements Filter {
080
081    // *********************************************************************
082    // Constants
083
084    /** Session attribute in which the username is stored */
085    public final static String CAS_FILTER_USER = "edu.yale.its.tp.cas.client.filter.user";
086
087    // *********************************************************************
088    // Configuration state
089
090    private String casLogin, casValidate, casAuthorizedProxy, casServiceUrl, casRenew, casServerName;
091
092    private boolean wrapRequest;
093
094    // *********************************************************************
095    // Initialization
096
097    public void init(FilterConfig config) throws ServletException {
098        casLogin = config.getInitParameter("edu.yale.its.tp.cas.client.filter.loginUrl");
099        casValidate = config.getInitParameter("edu.yale.its.tp.cas.client.filter.validateUrl");
100        casServiceUrl = config.getInitParameter("edu.yale.its.tp.cas.client.filter.serviceUrl");
101        casAuthorizedProxy = config.getInitParameter("edu.yale.its.tp.cas.client.filter.authorizedProxy");
102        casRenew = config.getInitParameter("edu.yale.its.tp.cas.client.filter.renew");
103        casServerName = config.getInitParameter("edu.yale.its.tp.cas.client.filter.serverName");
104        wrapRequest = Boolean.valueOf(config.getInitParameter("edu.yale.its.tp.cas.client.filter.wrapRequest")).booleanValue();
105    }
106
107    // *********************************************************************
108    // Filter processing
109
110    public void doFilter(ServletRequest request, ServletResponse response, FilterChain fc) throws ServletException,
111            IOException {
112
113        // make sure we've got an HTTP request
114        if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse))
115            throw new ServletException("CASFilter protects only HTTP resources");
116
117        // Wrap the request if desired
118        if (wrapRequest) {
119            request = new CASFilterRequestWrapper((HttpServletRequest) request);
120        }
121
122        HttpSession session = ((HttpServletRequest) request).getSession();
123
124        // if our attribute's already present, don't do anything
125        if (session != null && session.getAttribute(CAS_FILTER_USER) != null) {
126            fc.doFilter(request, response);
127            return;
128        }
129
130        // otherwise, we need to authenticate via CAS
131        String ticket = request.getParameter("ticket");
132
133        // no ticket? abort request processing and redirect
134        if (ticket == null || ticket.equals("")) {
135            if (casLogin == null) {
136                throw new ServletException("When CASFilter protects pages that do not receive a 'ticket' "
137                        + "parameter, it needs a edu.yale.its.tp.cas.client.filter.loginUrl " + "filter parameter");
138            }
139            ((HttpServletResponse) response).sendRedirect(casLogin + "?service="
140                    + getService((HttpServletRequest) request)
141                    + ((casRenew != null && !casRenew.equals("")) ? "&renew=" + casRenew : ""));
142
143            // abort chain
144            return;
145        }
146
147        // Yay, ticket! Validate it.
148        String user = getAuthenticatedUser((HttpServletRequest) request);
149        if (user == null)
150            throw new ServletException("Unexpected CAS authentication error");
151
152        // Store the authenticated user in the session
153        if (session != null) // probably unncessary
154            session.setAttribute(CAS_FILTER_USER, user);
155
156        // continue processing the request
157        fc.doFilter(request, response);
158    }
159
160    // *********************************************************************
161    // Destruction
162
163    public void destroy() {
164    }
165
166    // *********************************************************************
167    // Utility methods
168
169    /**
170     * Converts a ticket parameter to a username, taking into account an optionally configured trusted proxy in the tier
171     * immediately in front of us.
172     */
173    private String getAuthenticatedUser(HttpServletRequest request) throws ServletException {
174        ProxyTicketValidator pv = null;
175        try {
176            pv = new ProxyTicketValidator();
177            pv.setCasValidateUrl(casValidate);
178            pv.setServiceTicket(request.getParameter("ticket"));
179            pv.setService(getService(request));
180            pv.setRenew(Boolean.valueOf(casRenew).booleanValue());
181            pv.validate();
182            if (!pv.isAuthenticationSuccesful())
183                throw new ServletException("CAS authentication error: " + pv.getErrorCode() + ": "
184                        + pv.getErrorMessage());
185            if (pv.getProxyList().size() != 0) {
186                // ticket was proxied
187                if (casAuthorizedProxy == null) {
188                    throw new ServletException("this page does not accept proxied tickets");
189                } else {
190                    boolean authorized = false;
191                    String proxy = (String) pv.getProxyList().get(0);
192                    StringTokenizer casProxies = new StringTokenizer(casAuthorizedProxy);
193                    while (casProxies.hasMoreTokens()) {
194                        if (proxy.equals(casProxies.nextToken())) {
195                            authorized = true;
196                            break;
197                        }
198                    }
199                    if (!authorized) {
200                        throw new ServletException("unauthorized top-level proxy: '" + pv.getProxyList().get(0) + "'");
201                    }
202                }
203            }
204            return pv.getUser();
205        } catch (SAXException ex) {
206            String xmlResponse = "";
207            if (pv != null)
208                xmlResponse = pv.getResponse();
209            throw new ServletException(ex + " " + xmlResponse);
210        } catch (ParserConfigurationException ex) {
211            throw new ServletException(ex);
212        } catch (IOException ex) {
213            throw new ServletException(ex);
214        }
215    }
216
217    /**
218     * Returns either the configured service or figures it out for the current request. The returned service is
219     * URL-encoded.
220     */
221    private String getService(HttpServletRequest request) throws ServletException {
222        // ensure we have a server name or service name
223        if (casServerName == null && casServiceUrl == null)
224            throw new ServletException("need one of the following configuration "
225                    + "parameters: edu.yale.its.tp.cas.client.filter.serviceUrl or "
226                    + "edu.yale.its.tp.cas.client.filter.serverName");
227
228        // use the given string if it's provided
229        if (casServiceUrl != null)
230            return URLEncoder.encode(casServiceUrl);
231        else
232            // otherwise, return our best guess at the service
233            return Util.getService(request, casServerName);
234    }
235}