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 javax.xml.parsers.ParserConfigurationException;
042import org.xml.sax.SAXException;
043
044/**
045 * <p>
046 * Protects web-accessible resources with CAS.
047 * </p>
048 * <p>
049 * The following filter initialization parameters are declared in <code>web.xml</code>:
050 * </p>
051 * <ul>
052 * <li><code>edu.yale.its.tp.cas.client.filter.loginUrl</code>: URL to login page on CAS server. (Required)</li>
053 * <li><code>edu.yale.its.tp.cas.client.filter.validateUrl</code>: URL to validation URL on CAS server. (Required)</li>
054 * <li><code>edu.yale.its.tp.cas.client.filter.serviceUrl</code>: URL of this service. (Required if
055 * <code>serverName</code> is not specified)</li>
056 * <li><code>edu.yale.its.tp.cas.client.filter.serverName</code>: full hostname with port number (e.g.
057 * <code>www.foo.com:8080</code>). Port number isn't required if it is standard (80 for HTTP, 443 for HTTPS). (Required
058 * if <code>serviceUrl</code> is not specified)</li>
059 * <li><code>edu.yale.its.tp.cas.client.filter.authorizedProxy</code>: whitespace-delimited list of valid proxies
060 * through which authentication may have proceeded. One one proxy must match. (Optional. If nothing is specified, the
061 * filter will only accept service tickets &#150; not proxy tickets.)</li>
062 * <li><code>edu.yale.its.tp.cas.client.filter.renew</code>: value of CAS "renew" parameter. Bypasses single sign-on and
063 * requires user to provide CAS with his/her credentials again. (Optional. If nothing is specified, this defaults to
064 * false.)</li>
065 * <li><code>edu.yale.its.tp.cas.client.filter.wrapRequest</code>: wrap the <code>HttpServletRequest</code> object,
066 * overriding the <code>getRemoteUser()</code> method. When set to "true", <code>request.getRemoteUser()</code> will
067 * return the username of the currently logged-in CAS user. (Optional. If nothing is specified, this defaults to false.)
068 * </li>
069 * </ul>
070 * <p>
071 * The logged-in username is set in the session attribute defined by the value of <code>CAS_FILTER_USER</code> and may
072 * be accessed from within your application either by setting <code>wrapRequest</code> and calling
073 * <code>request.getRemoteUser()</code>, or by calling <code>session.getAttribute(CASFilter.CAS_FILTER_USER)</code>.
074 * </p>
075 *
076 * @author Shawn Bayern
077 */
078public class CASFilter implements Filter {
079
080    // *********************************************************************
081    // Constants
082
083    /** Session attribute in which the username is stored */
084    public final static String CAS_FILTER_USER = "edu.yale.its.tp.cas.client.filter.user";
085
086    // *********************************************************************
087    // Configuration state
088
089    private String casLogin, casValidate, casAuthorizedProxy, casServiceUrl, casRenew, casServerName;
090
091    private boolean wrapRequest;
092
093    // *********************************************************************
094    // Initialization
095
096    @Override
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    @Override
111    public void doFilter(ServletRequest request, ServletResponse response, FilterChain fc) throws ServletException,
112            IOException {
113
114        // make sure we've got an HTTP request
115        if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse))
116            throw new ServletException("CASFilter protects only HTTP resources");
117
118        // Wrap the request if desired
119        if (wrapRequest) {
120            request = new CASFilterRequestWrapper((HttpServletRequest) request);
121        }
122
123        HttpSession session = ((HttpServletRequest) request).getSession();
124
125        // if our attribute's already present, don't do anything
126        if (session != null && session.getAttribute(CAS_FILTER_USER) != null) {
127            fc.doFilter(request, response);
128            return;
129        }
130
131        // otherwise, we need to authenticate via CAS
132        String ticket = request.getParameter("ticket");
133
134        // no ticket? abort request processing and redirect
135        if (ticket == null || ticket.equals("")) {
136            if (casLogin == null) {
137                throw new ServletException("When CASFilter protects pages that do not receive a 'ticket' "
138                        + "parameter, it needs a edu.yale.its.tp.cas.client.filter.loginUrl " + "filter parameter");
139            }
140            ((HttpServletResponse) response).sendRedirect(casLogin + "?service="
141                    + getService((HttpServletRequest) request)
142                    + ((casRenew != null && !casRenew.equals("")) ? "&renew=" + casRenew : ""));
143
144            // abort chain
145            return;
146        }
147
148        // Yay, ticket! Validate it.
149        String user = getAuthenticatedUser((HttpServletRequest) request);
150        if (user == null)
151            throw new ServletException("Unexpected CAS authentication error");
152
153        // Store the authenticated user in the session
154        if (session != null) // probably unncessary
155            session.setAttribute(CAS_FILTER_USER, user);
156
157        // continue processing the request
158        fc.doFilter(request, response);
159    }
160
161    // *********************************************************************
162    // Destruction
163
164    @Override
165    public void destroy() {
166    }
167
168    // *********************************************************************
169    // Utility methods
170
171    /**
172     * Converts a ticket parameter to a username, taking into account an optionally configured trusted proxy in the tier
173     * immediately in front of us.
174     */
175    private String getAuthenticatedUser(HttpServletRequest request) throws ServletException {
176        ProxyTicketValidator pv = null;
177        try {
178            pv = new ProxyTicketValidator();
179            pv.setCasValidateUrl(casValidate);
180            pv.setServiceTicket(request.getParameter("ticket"));
181            pv.setService(getService(request));
182            pv.setRenew(Boolean.valueOf(casRenew).booleanValue());
183            pv.validate();
184            if (!pv.isAuthenticationSuccesful())
185                throw new ServletException("CAS authentication error: " + pv.getErrorCode() + ": "
186                        + pv.getErrorMessage());
187            if (pv.getProxyList().size() != 0) {
188                // ticket was proxied
189                if (casAuthorizedProxy == null) {
190                    throw new ServletException("this page does not accept proxied tickets");
191                } else {
192                    boolean authorized = false;
193                    String proxy = (String) pv.getProxyList().get(0);
194                    StringTokenizer casProxies = new StringTokenizer(casAuthorizedProxy);
195                    while (casProxies.hasMoreTokens()) {
196                        if (proxy.equals(casProxies.nextToken())) {
197                            authorized = true;
198                            break;
199                        }
200                    }
201                    if (!authorized) {
202                        throw new ServletException("unauthorized top-level proxy: '" + pv.getProxyList().get(0) + "'");
203                    }
204                }
205            }
206            return pv.getUser();
207        } catch (SAXException ex) {
208            String xmlResponse = "";
209            if (pv != null)
210                xmlResponse = pv.getResponse();
211            throw new ServletException(ex + " " + xmlResponse);
212        } catch (ParserConfigurationException ex) {
213            throw new ServletException(ex);
214        } catch (IOException ex) {
215            throw new ServletException(ex);
216        }
217    }
218
219    /**
220     * Returns either the configured service or figures it out for the current request. The returned service is
221     * URL-encoded.
222     */
223    private String getService(HttpServletRequest request) throws ServletException {
224        // ensure we have a server name or service name
225        if (casServerName == null && casServiceUrl == null)
226            throw new ServletException("need one of the following configuration "
227                    + "parameters: edu.yale.its.tp.cas.client.filter.serviceUrl or "
228                    + "edu.yale.its.tp.cas.client.filter.serverName");
229
230        // use the given string if it's provided
231        if (casServiceUrl != null)
232            return URLEncoder.encode(casServiceUrl);
233        else
234            // otherwise, return our best guess at the service
235            return Util.getService(request, casServerName);
236    }
237}