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 – 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}