001: /*
002: * Copyright 2005-2006 The Kuali Foundation.
003: *
004: * Licensed under the Educational Community License, Version 1.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.opensource.org/licenses/ecl1.php
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016: package org.kuali.core.web.filter;
017:
018: import java.io.BufferedReader;
019: import java.io.IOException;
020: import java.io.InputStreamReader;
021: import java.io.PrintWriter;
022: import java.net.URL;
023: import java.util.Enumeration;
024: import java.util.HashMap;
025:
026: import javax.servlet.Filter;
027: import javax.servlet.FilterChain;
028: import javax.servlet.FilterConfig;
029: import javax.servlet.ServletException;
030: import javax.servlet.ServletRequest;
031: import javax.servlet.ServletResponse;
032: import javax.servlet.http.HttpServletRequest;
033: import javax.servlet.http.HttpServletResponse;
034: import javax.servlet.http.HttpSession;
035:
036: import org.apache.commons.lang.StringUtils;
037: import org.apache.log4j.Logger;
038:
039: /**
040: * This class is the Filter to use CAS for authentication.
041: *
042: *
043: */
044:
045: /**
046: * <p>
047: * Title: CASFilter
048: * </p>
049: * <p>
050: * Description: Filter for Servlet 2.3 spec server to use CAS for authentication. Map any url pattern to this filter to support CAS
051: * authentication for that url pattern. If login is successful at CAS the FilterCASBean will be available to you in the user's
052: * session under attribute name "filterCASBean".
053: * </p>
054: * <p>
055: * Authenticates a user by redirecting their browser to CAS for authentication. CAS puts casticket parameter on querystring after
056: * successful login. The value of casticket is verified against CAS using https. If the casticket is valid we put the user's user
057: * name and any key/value pairs returned by CAS (from the https request verifying casticket) into the FilterCASBean java bean and
058: * save the bean in the session. Now we can check against nullness of this bean to verify user authentication.
059: * </p>
060: * The following init parameters are needed for each instance of this filter. These are placed in the web.xml file.
061: *
062: * <filter> <filter-name>cas</filter-name> <filter-class>org.kuali.web.filter.UHCASFilter</filter-class> <init-param>
063: * <param-name>serviceParamName</param-name> <param-value>service</param-value> </init-param> <init-param>
064: * <param-name>ticketParamName</param-name> <param-value>ticket</param-value> </init-param> <init-param> <param-name>validationURL</param-name>
065: * <param-value>https://login.its.hawaii.edu:8445/cas/validate</param-value> </init-param> <init-param> <param-name>loginURL</param-name>
066: * <param-value>https://login.its.hawaii.edu:8445/cas/login</param-value> </init-param> <init-param> <param-name>logoutURL</param-name>
067: * <param-value>https://login.its.hawaii.edu:8445/cas/logout</param-value> </init-param> </filter>
068: *
069: * <filter-mapping> <filter-name>cas</filter-name> <servlet-name>action</servlet-name> </filter-mapping>
070: *
071: * TODO: add simple param validation
072: *
073: * TODO: rebuild it to work with either single or multiple URL params
074: *
075: * IU's CAS server receives a URL of the form:
076: * https://cas.iu.edu/cas/login?cassvc=MYANY&casurl=https://onestart.iu.edu:443/my-prd/Kerberos/Login.do generates a URL of the
077: * form: https://onestart.iu.edu:443/my-prd/Portal.do?casticket=ST-285420-LYbpu3QKAjyC7D468WS2& UH's CAS server receives a URL of
078: * the form: https://login.its.hawaii.edu:8445/cas/login?service=https://localhost:8443/casTest/casLogin.do generates a URL of the
079: * form: https://localhost:8443/casTest/casLogin.do?ticket='ST-492-fzmviDIliftbdJrF1Q30'
080: */
081: public class UHCasFilter implements Filter {
082: private static Logger LOG = Logger.getLogger(KualiCasFilter.class);
083:
084: public static final String USERNAME_HASH = "edu.hawaii.its.filter.UsernameHash";
085: public static final String USERNAME_HASH_KEY = "edu.hawaii.its.filter.BaseContext";
086:
087: private String serviceParamName;
088: private String ticketParamName;
089:
090: private String validationURL;
091: private String loginURL;
092: private String logoutURL;
093:
094: private String autoLoginUserName;
095: private String beginPostPage;
096: private String endPostPage;
097:
098: /**
099: * Initialize filter constansts
100: *
101: * @param filterConfig
102: * @throws ServletException
103: */
104: public void init(FilterConfig filterConfig) throws ServletException {
105: System.setProperty("java.protocol.handler.pkgs",
106: "com.sun.net.ssl.internal.www.protocol");
107:
108: autoLoginUserName = filterConfig
109: .getInitParameter("autoLoginUserName");
110:
111: serviceParamName = filterConfig
112: .getInitParameter("serviceParamName");
113: ticketParamName = filterConfig
114: .getInitParameter("ticketParamName");
115:
116: validationURL = filterConfig.getInitParameter("validationURL");
117: loginURL = filterConfig.getInitParameter("loginURL");
118: logoutURL = filterConfig.getInitParameter("logoutURL");
119:
120: beginPostPage = "<html><head><script>\n" + "function pf(){\n"
121: + "\tdocument.f.submit();\n}" + "\n</script>\n</head>"
122: + "<title>Auth Redirect</title>"
123: + "<body onload=\"pf()\">\n";
124: endPostPage = "<input type='submit'></form></body></html>";
125:
126: LOG.debug("FilterCAS: init() ValidationURL: " + validationURL
127: + "\nLogin URL: " + loginURL);
128: }
129:
130: /**
131: * Intercepts any requesting url pattern it's mapped to and directs traffic according to user's current step in authentication
132: * process
133: *
134: * @param request
135: * @param response
136: * @param chain
137: * @throws IOException
138: * @throws ServletException
139: */
140: public void doFilter(ServletRequest request,
141: ServletResponse response, FilterChain chain)
142: throws IOException, ServletException {
143:
144: HttpServletRequest hrequest = (HttpServletRequest) request;
145: HttpSession session = hrequest.getSession();
146:
147: HashMap userList = (HashMap) session
148: .getAttribute(USERNAME_HASH);
149:
150: if (userList == null) {
151: userList = new HashMap();
152: session.setAttribute(USERNAME_HASH, userList);
153: }
154:
155: // If the username list has the cassvc they've been authenticated just pass request on
156: String baseContext = hrequest.getContextPath();
157: String serviceName = hrequest.getRequestURL().toString();
158:
159: if (userList.get(baseContext) != null) {
160: LOG.debug("CASFilter doFilter(): Already Authenticated");
161: request.setAttribute(USERNAME_HASH_KEY, baseContext);
162: chain.doFilter(request, response);
163: } else {
164: if (!StringUtils.isBlank(autoLoginUserName)) {
165: userList.put(baseContext, autoLoginUserName);
166: request.setAttribute(USERNAME_HASH_KEY, baseContext);
167: LOG.debug("CASFilter doFilter(): autoLoginUserName = "
168: + autoLoginUserName);
169: LOG
170: .info("CASFilter doFilter(): spoofed authentication successful");
171: chain.doFilter(request, response);
172: } else {
173: String casticket = hrequest
174: .getParameter(ticketParamName);
175: LOG.debug("CASFilter doFilter(): " + ticketParamName
176: + "=" + casticket);
177: if (casticket == null) {
178: // user hasn't been to CAS yet redirect them
179: LOG
180: .debug("CASFilter doFilter(): no casticket redirecting browser to CAS Server");
181: redirect(hrequest, (HttpServletResponse) response);
182: } else {
183: // user has been to CAS but casticket hasn't been verified, otherwise
184: // we'd have filterCASBean in session
185: LOG
186: .debug("CASFilter doFilter(): casticket exists verifying it");
187: String username = null;
188: try {
189: username = validate(serviceName, casticket);
190: } catch (IOException ex) {
191: LOG
192: .error("CASFilter doFilter(): Error validating casticket");
193: username = null;
194: }
195:
196: if (username == null) {
197: // failed validation, bad casticket, user is going back to CAS to login
198: // and get new casticket
199: LOG
200: .debug("CASFilter doFilter(): casticket invalid redirect to browser");
201: LOG
202: .debug("CASFilter doFilter(): query_string = "
203: + hrequest.getQueryString());
204: redirect(hrequest,
205: (HttpServletResponse) response);
206: } else {
207: // user's casticket verified as good, adding to username list
208: // and passing request on
209: userList.put(baseContext, username);
210: request.setAttribute(USERNAME_HASH_KEY,
211: baseContext);
212: LOG.debug("CASFilter doFilter(): username = "
213: + username);
214: LOG
215: .debug("CASFilter doFilter(): authentication successful");
216: chain.doFilter(request, response);
217: }
218: }
219: }
220: }
221: }
222:
223: /**
224: * Determines if request method is get or post and redirects browser to CAS accordingly.
225: *
226: * @param hrequest
227: * @param hresponse
228: * @throws IOException
229: */
230: private void redirect(HttpServletRequest hrequest,
231: HttpServletResponse hresponse) throws java.io.IOException {
232:
233: LOG.debug("CASFilter redirect(): Beginning Redirect");
234: String method = hrequest.getMethod();
235: if (method.equals("POST")) {
236: sendPostRedirect(hrequest, hresponse);
237: } else {
238: sendGetRedirect(hrequest, hresponse);
239: }
240: }
241:
242: /**
243: * redirects post request to CAS. Puts all params, including those on querystring, removes any bad casticket params. Didn't
244: * preserve querystring because still boils down to requesting params, form or querystring based, through request
245: *
246: * @param hrequest
247: * @param hresponse
248: * @throws IOException
249: */
250: private void sendPostRedirect(HttpServletRequest hrequest,
251: HttpServletResponse hresponse) throws java.io.IOException {
252:
253: StringBuffer casURLBuf = hrequest.getRequestURL();
254: String redirectURL = loginURL + "?" + serviceParamName + "="
255: + casURLBuf.toString();
256: PrintWriter out = hresponse.getWriter();
257: out.print(beginPostPage);
258: out.print("<form action=\"" + redirectURL
259: + "\" method=\"post\" name=\"f\">");
260:
261: /*
262: * Preserve all request parameters in hidden form fields, page will send form to CAS with body onLoad dhtml event (see
263: * String beginPostPage) Strip off any bad casticket coming our way
264: */
265: StringBuffer formParams = new StringBuffer();
266: String parameterName;
267: String[] parameterVals;
268: Enumeration parameterEnum = hrequest.getParameterNames();
269: while (parameterEnum.hasMoreElements()) {
270: parameterName = (String) parameterEnum.nextElement();
271: parameterVals = hrequest.getParameterValues(parameterName);
272: if (!parameterName.equals(ticketParamName)) {
273: for (int i = 0; i < parameterVals.length; i++) {
274: formParams.append("<input type=\"hidden\" name=\"");
275: formParams.append(parameterName);
276: formParams.append("\" value=\"");
277: formParams.append(parameterVals[i]);
278: formParams.append("\">\n");
279: }
280: }
281: }
282: LOG
283: .debug("CASFilter sendPostRedirect(): Sending POST redirect");
284: out.print(formParams.toString());
285: out.print(endPostPage);
286: }
287:
288: /**
289: * redirects get request to CAS. Builds URL requested minus any casticket params
290: *
291: * @param hrequest
292: * @param hresponse
293: * @throws IOException
294: */
295: private void sendGetRedirect(HttpServletRequest hrequest,
296: HttpServletResponse hresponse) throws java.io.IOException {
297:
298: StringBuffer queryStringBuf = new StringBuffer();
299: String[] values = null;
300: String paramName = null;
301: Enumeration paramEnum = hrequest.getParameterNames();
302: int cnt = 0;
303: // Strip off existing bad casticket if one exists
304: // otherwise we'll always pick up the first one each time, which is bad
305: // causing redirect loop
306: while (paramEnum.hasMoreElements()) {
307: paramName = (String) paramEnum.nextElement();
308: if (!paramName.equals(ticketParamName)) {
309: values = hrequest.getParameterValues(paramName);
310: if (cnt > 0) {
311: queryStringBuf.append("&");
312: }
313: for (int i = 0; i < values.length; i++) {
314: if (i > 0) {
315: queryStringBuf.append("&");
316: }
317: queryStringBuf.append(paramName);
318: queryStringBuf.append("=");
319: queryStringBuf.append(values[i]);
320: }
321: cnt++;
322: }
323: }
324:
325: // build entire redirect url and send to client
326: StringBuffer redirectURL = new StringBuffer();
327: redirectURL.append(loginURL);
328: redirectURL.append("?" + serviceParamName + "="
329: + hrequest.getRequestURL().toString());
330:
331: if (!StringUtils.isEmpty(queryStringBuf.toString())) {
332: redirectURL.append("&");
333: redirectURL.append(queryStringBuf.toString());
334: }
335: LOG.debug("CASFilter sendGetRedirect(): Sending GET redirect");
336: hresponse.sendRedirect(redirectURL.toString());
337: }
338:
339: /**
340: * Open a stream using https to validate a CAS ticket against the service. Return a FilterCASBean set with username and any
341: * key/value pairs returned by CAS. If connection fails or CAS returns "no" return null.
342: *
343: * @param service The service in CAS this filter is set to validate against
344: * @param ticket The ticket CAS puts as querystring parameter before redirecting browser back to this server
345: * @exception IOException if an input/output error occurs
346: * @return null if casticket invalid or FilterCASBean
347: */
348: private String validate(String service, String ticket)
349: throws java.io.IOException {
350: String result = null;
351:
352: String casValURL = validationURL + "?" + serviceParamName + "="
353: + service + "&" + ticketParamName + "=" + ticket;
354: // TODO: if you have additional parameters, add them in here
355:
356: LOG.debug("CASFilter validate(): validate URL = " + casValURL);
357: URL u = new URL(casValURL);
358: BufferedReader in = new BufferedReader(new InputStreamReader(u
359: .openStream()));
360: if (in != null) {
361: String line1 = in.readLine();
362: String line2 = in.readLine();
363:
364: if (!"no".equals(line1)) {
365: result = line2;
366: }
367:
368: try {
369: in.close();
370: } catch (IOException e) {
371: LOG.error("caught IOException closing validation URL: "
372: + e.getMessage());
373: }
374: }
375:
376: return result;
377: }
378:
379: /**
380: * This isn't needed for this application, but required to implement the Filter class.
381: */
382: public void destroy() {
383: // this space intentionally left blank
384: }
385:
386: public static String getRemoteUser(HttpServletRequest request) {
387: String cassvc = (String) request
388: .getAttribute(USERNAME_HASH_KEY);
389: if (cassvc == null) {
390: // Don't know what to do
391: return null;
392: }
393:
394: HashMap userList = (HashMap) request.getSession().getAttribute(
395: USERNAME_HASH);
396:
397: if (userList == null) {
398: // Again, Don't know what to do
399: return null;
400: }
401:
402: String username = (String) userList.get(cassvc);
403: return username;
404: }
405: }
|