View Javadoc

1   /*
2    * Copyright (c) 2007 Kathryn Huxtable
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   *
16   * $Id: FilterHandler.java 1194 2007-04-16 18:03:39Z kathryn5 $
17   */
18  package org.kathrynhuxtable.middleware.shibshim.filter;
19  
20  import java.io.IOException;
21  import java.io.UnsupportedEncodingException;
22  import java.util.Date;
23  
24  import javax.servlet.FilterChain;
25  import javax.servlet.ServletException;
26  import javax.servlet.ServletRequest;
27  import javax.servlet.ServletResponse;
28  import javax.servlet.http.HttpServletRequest;
29  import javax.servlet.http.HttpServletResponse;
30  import javax.servlet.http.HttpSession;
31  
32  import org.apache.log4j.Logger;
33  import org.kathrynhuxtable.middleware.shibshim.util.AESCrypt;
34  import org.kathrynhuxtable.middleware.shibshim.util.AESCryptException;
35  import org.kathrynhuxtable.middleware.shibshim.util.Base64;
36  import org.kathrynhuxtable.middleware.shibshim.util.RSASignature;
37  import org.kathrynhuxtable.middleware.shibshim.util.RSASignatureException;
38  
39  /**
40   * Handle an individual Shibboleth Shim filter request. The only reason this
41   * class exists is to simplify the calls and to shrink the size of the files.
42   * This class is instantiated for each request, so the request and response
43   * objects can be instance variables rather than method variables.
44   */
45  public class FilterHandler {
46      /**
47       * Logger for error/information/debug messages.
48       */
49      private static Logger       log              = Logger.getLogger(FilterHandler.class.getName());
50  
51      /**
52       * Default SSL port.
53       */
54      private static final int    DEFAULT_SSL_PORT = 443;
55  
56      /**
57       * Saved copy of the request object.
58       */
59      private HttpServletRequest  request          = null;
60  
61      /**
62       * Saved copy of the response object.
63       */
64      private HttpServletResponse response         = null;
65  
66      /**
67       * Saved copy of the filter chain object.
68       */
69      private FilterChain         chain            = null;
70  
71      /**
72       * Saved copy of the filter object.
73       */
74      private ShibShimFilter      filter           = null;
75  
76      /**
77       * Saved session object.
78       */
79      private HttpSession         session          = null;
80  
81      /**
82       * Saved user attributes object.
83       */
84      private UserAttributesImpl  attributes       = null;
85  
86      /**
87       * Instantiate a FilterHandler. Save the request, response, chain, and
88       * filter for later use. Parse the application name from the request if not
89       * supplied.
90       * 
91       * @param req
92       *            the orginal request object.
93       * @param res
94       *            the original response object.
95       * @param chain
96       *            the original filter chain object.
97       * @param filter
98       *            the original filter object.
99       * @throws ImmediateReturnException
100      *             if we have already printed a redirect or an error page.
101      */
102     public FilterHandler(ServletRequest req, ServletResponse res, FilterChain chain, ShibShimFilter filter) throws ImmediateReturnException {
103         // The ShibShimFilter is currently defined only for HTTP and HTTPS.
104         if (!(req instanceof HttpServletRequest) || !(res instanceof HttpServletResponse)) {
105             ErrorPage.displayErrorPage(res, "The Shibboleth Shim filter only supports HTTP and HTTPS");
106         }
107 
108         // Cast the request and response to the Http forms.
109         request = (HttpServletRequest) req;
110         response = (HttpServletResponse) res;
111         log.debug("ServletPath = \"" + request.getServletPath() + "\"");
112 
113         this.chain = chain;
114         this.filter = filter;
115 
116         // Construct a default application name if none was supplied in the
117         // configuration.
118         if (filter.getApplication() == null) {
119             String application = request.getServerName();
120             if (request.getServerPort() != DEFAULT_SSL_PORT) {
121                 application += ":" + request.getServerPort();
122             }
123             application += request.getContextPath();
124             filter.setApplication(application);
125         }
126 
127         // Get any cached attributes that may be present.
128         session = request.getSession(false);
129         if (session != null) {
130             attributes = (UserAttributesImpl) session.getAttribute(filter.getSessionAttribute());
131         }
132     }
133 
134     /**
135      * <p>
136      * Filter requests to make sure user is logged in. If not, redirects to the
137      * Shibboleth Shim server, which will log the user in. If logged in, fills
138      * in user attributes that can be accessed by the filtered servlet as HTTP
139      * headers.
140      * </p>
141      * 
142      * @throws ImmediateReturnException
143      *             if we have printed a redirect or an error page.
144      */
145     public void filterRequest() throws ImmediateReturnException {
146         // Check for login/logout requests.
147         handleLogin();
148         handleLogout();
149 
150         // At this point, we know we may require an authenticated session.
151         // Create a new session if needed. Get its session ID.
152         if (session == null) {
153             session = request.getSession(true);
154         }
155 
156         // Retrieve attributes from session.
157         initializeAttributes(false);
158 
159         // Check for Assertion Consumer Service redirects.
160         // This will retrieve the attributes from the POST redirect from the
161         // Shibboleth Shim server.
162         handleACS();
163 
164         // Make sure that the application and sessionId in
165         // the attributes object is current.
166         attributes.put("config-application", filter.getApplication());
167 
168         // If our user attributes are missing or no longer current, retrieve new
169         // values.
170         long currentTime = new Date().getTime();
171         long verifyTime = getIntConfigHeader("config-verifytime", 0);
172 
173         log.debug("Do we need to get user info");
174         verifyUser(attributes.getHeader("handle"), currentTime, verifyTime);
175 
176         // If we have no session, but one is required, force the user to log in.
177         if (!attributes.hasAttributes() && filter.isSessionRequired()) {
178             String url = request.getRequestURL().toString();
179             String queryString = request.getQueryString();
180             if (queryString != null) {
181                 url += "?" + queryString;
182             }
183 
184             redirectToLogin(url);
185         }
186 
187         // Update time stamps in user attributes.
188         if (attributes.hasAttributes()) {
189             // Move currentTime into lastTime and set currentTime to the current
190             // time.
191             long oldCurrentTime = getIntConfigHeader("config-currenttime", currentTime);
192             attributes.put("config-lasttime", Long.toString(oldCurrentTime));
193             attributes.put("config-currenttime", Long.toString(currentTime));
194 
195             // NOPW Production shouldn't need this, especially for apps that get
196             // userPassword.
197             // log.debug("User info: \"" + attributes + "\"");
198         }
199 
200         // Redirect if we're handling an ACS, otherwise wrap the request to
201         // return the user attributes and chain down the filters.
202         if (checkForACSPath()) {
203             redirectToTarget();
204         } else {
205             wrapRequest();
206         }
207     }
208 
209     /**
210      * We've just logged in and set the attributes. Redirect to the original URL
211      * that the user wanted.
212      * 
213      * @throws ImmediateReturnException
214      *             because we have already printed a redirect.
215      */
216     private void redirectToTarget() throws ImmediateReturnException {
217         String url = request.getParameter("target");
218         if (url == null) {
219             url = "";
220         } else if (url.charAt(0) != '/') {
221             url = request.getContextPath() + "/" + url;
222         } else {
223             url = request.getContextPath() + url;
224         }
225         generalShibShimRedirect(url);
226     }
227 
228     /**
229      * Wrap the request to return principal attributes and pass the request and
230      * response along the filter chain.
231      * 
232      * @throws ImmediateReturnException
233      *             if we have printed an error page.
234      */
235     private void wrapRequest() throws ImmediateReturnException {
236         RequestWrapper requestWrapper = new RequestWrapper(request, attributes, filter.getRemoteUserAttribute());
237 
238         try {
239             chain.doFilter(requestWrapper, response);
240         } catch (ServletException se) {
241             ErrorPage.displayErrorPage(response, "Error protecting application: " + se);
242         } catch (IOException ie) {
243             ErrorPage.displayErrorPage(response, "Error protecting application: " + ie);
244         }
245     }
246 
247     /**
248      * Is the request for the local Assertion Consumer Service?
249      * 
250      * @return true if the URL is for the local Assertion Consumer Service.
251      */
252     private boolean checkForACSPath() {
253         return request.getServletPath().equals(filter.getLocalAcsPath());
254     }
255 
256     /**
257      * Parse the attributes from the XML document and verify the signature.
258      * 
259      * @throws ImmediateReturnException
260      *             if we print an error page.
261      */
262     private void handleACS() throws ImmediateReturnException {
263         if (!checkForACSPath()) {
264             return;
265         }
266 
267         attributes.clearUserInfo();
268         log.debug("Trying to get user info");
269         try {
270             String userInfo = extractAndVerifyUserInfo();
271             attributes.parseUserInfo(userInfo);
272             attributes.put("config-attributes", userInfo);
273         } catch (UserInfoException e) {
274             ErrorPage.displayErrorPage(response, e.getMessage());
275         }
276         log.debug("Back from getting user info");
277     }
278 
279     /**
280      * Is the request for the login path, requiring the user to log in?
281      * 
282      * @return true if the request is for the login path.
283      */
284     private boolean checkForLoginPath() {
285         return request.getServletPath().equals(filter.getLoginRedirectPath());
286     }
287 
288     /**
289      * Process forced login. Make sure there is a context session and session
290      * attributes, and clear the session attribute data, if any before
291      * redirecting to the Shibboleth Shim server.
292      * 
293      * @throws ImmediateReturnException
294      *             if we print a redirect or an error page.
295      */
296     private void handleLogin() throws ImmediateReturnException {
297         if (!checkForLoginPath()) {
298             return;
299         }
300 
301         // Make sure we have a session established and clear the attributes.
302         if (session == null) {
303             session = request.getSession(true);
304         }
305         initializeAttributes(true);
306 
307         // Get the desired target URL.
308         String target = request.getParameter("target");
309         if (target == null) {
310             target = filter.getApplication();
311         } else {
312             try {
313                 target = java.net.URLDecoder.decode(target, "US-ASCII");
314             } catch (IOException ie) {
315                 log.error("IOException encountered decoding target: " + ie);
316                 target = filter.getApplication();
317             }
318         }
319 
320         // Redirect to the Shibboleth Shim filter to log the user in if
321         // necessary.
322         String force = request.getParameter("force");
323         if (force != null && force.equalsIgnoreCase("true")) {
324             redirectToForceLogin(session.getId(), target);
325         } else {
326             redirectToLogin(target);
327         }
328     }
329 
330     /**
331      * Is the request for the logout path, forcing a logout?
332      * 
333      * @return true if the request is for the logout path.
334      */
335     private boolean checkForLogoutPath() {
336         return request.getServletPath().equals(filter.getLogoutRedirectPath());
337     }
338 
339     /**
340      * Process logout. Invalidate the servlet session, if any, and redirect to
341      * ShibShim logout, which will destroy the main session data. Note that
342      * invalidating the context session will also destroy the ShibShim session.
343      * 
344      * @throws ImmediateReturnException
345      *             if we print a redirect.
346      */
347     private void handleLogout() throws ImmediateReturnException {
348         if (!checkForLogoutPath()) {
349             return;
350         }
351 
352         // Invalidate the server session.
353         if (session != null) {
354             session.invalidate();
355         }
356 
357         // Redirect to the Shibboleth Shim server, specifying a target URL.
358         String target = request.getParameter("target");
359         if (target != null) {
360             try {
361                 target = java.net.URLDecoder.decode(target, "US-ASCII");
362             } catch (IOException ie) {
363                 log.error("IOException encountered decoding target: " + ie);
364                 target = "";
365             }
366         }
367         redirectToLogout(target);
368     }
369 
370     /**
371      * Is it time to verify with the Shibboleth Shim server that the user's
372      * session is still logged in?
373      * 
374      * @param currentTime
375      *            the current time.
376      * @param verifyTime
377      *            the last time the session was verified.
378      * @return true if it is time to verify the session.
379      */
380     private boolean isTimeToVerifyAuthentication(long currentTime, long verifyTime) {
381         // TODO need to verify session
382         // The check is
383         // (!attributes.hasAttributes() || verifyTime == 0 || currentTime - verifyTime >= filter.getTimeout())
384         return false;
385     }
386 
387     /**
388      * Verify the session. At the moment this does nothing.
389      * 
390      * @param handle
391      *            the session handle for the Shibboleth Shim server.
392      * @param currentTime
393      *            the current time.
394      * @param verifyTime
395      *            the last time the session was verified.
396      * @throws ImmediateReturnException
397      *             if we print a redirect.
398      */
399     private void verifyUser(String handle, long currentTime, long verifyTime) throws ImmediateReturnException {
400         if (!isTimeToVerifyAuthentication(currentTime, verifyTime)) {
401             return;
402         }
403     }
404 
405     /**
406      * Redirects browser to the Shibboleth Shim server to perform a login.
407      * 
408      * @param url
409      *            the URL to redirect to after login/out.
410      * 
411      * @throws ImmediateReturnException
412      *             if we print a redirect or an error page.
413      */
414     protected void redirectToLogin(String url) throws ImmediateReturnException {
415         String acs = filter.getAcsUrl();
416         int start = 0;
417         if (url.startsWith("https://")) {
418             acs += "/https/";
419             start = "https://".length();
420         } else if (url.startsWith("http://")) {
421             acs += "/http/";
422             start = "http://".length();
423         }
424 
425         int slash = url.indexOf('/', start);
426         if (slash < 0) {
427             slash = url.length();
428         }
429         acs += url.substring(start, slash) + request.getContextPath() + filter.getLocalAcsPath();
430 
431         String target = url.replaceFirst("^https?://[^/]+" + request.getContextPath(), "");
432 
433         // The url https://myhost.example.edu/foo/protected/index.jsp yields:
434         //
435         // acs = /https/myhost.example.edu/foo/localACS
436         //
437         // target = /protected/index.jsp
438 
439         log.debug("User info is still empty");
440         try {
441             String redirectUrl = acs + "?target=" + java.net.URLEncoder.encode(target, "US-ASCII");
442             generalShibShimRedirect(redirectUrl);
443         } catch (UnsupportedEncodingException e) {
444             ErrorPage.displayErrorPage(response, "Got error attempting to log in");
445         }
446     }
447 
448     /**
449      * Redirects browser to the Shibboleth Shim server and force login.
450      * @param sessionId
451      *            the session ID to invalidate on logout.
452      * @param returnUrl
453      *            the URL to redirect to after login/out.
454      * 
455      * @throws ImmediateReturnException
456      *             if we print a redirect or an error page.
457      */
458     protected void redirectToForceLogin(String sessionId, String returnUrl) throws ImmediateReturnException {
459         try {
460             String url = filter.getAcsUrl() + "?" + "return=" + java.net.URLEncoder.encode(returnUrl, "US-ASCII");
461             generalShibShimRedirect(url);
462         } catch (UnsupportedEncodingException e) {
463             ErrorPage.displayErrorPage(response, "Got error attempting to log in");
464         }
465     }
466 
467     /**
468      * Redirects browser to Shibboleth Shim server to force a logout.
469      * @param returnUrl
470      *            the URL to redirect to after login/out.
471      * 
472      * @throws ImmediateReturnException
473      *             if we print a redirect or an error page.
474      */
475     protected void redirectToLogout(String returnUrl) throws ImmediateReturnException {
476         try {
477             String url = filter.getAcsUrl();
478             if (returnUrl != null) {
479                 url += "?return=" + java.net.URLEncoder.encode(returnUrl, "US-ASCII");
480             }
481             generalShibShimRedirect(url);
482         } catch (UnsupportedEncodingException e) {
483             ErrorPage.displayErrorPage(response, "Got error attempting to log out");
484         }
485     }
486 
487     /**
488      * Redirects browser. Used by the other redirect methods.
489      * @param url
490      *            the URL to redirect to after login/out.
491      * 
492      * @throws ImmediateReturnException
493      *             because we printed a redirect.
494      */
495     protected void generalShibShimRedirect(String url) throws ImmediateReturnException {
496         try {
497             log.debug("Redirecting to \"" + url + "\"");
498             response.sendRedirect(url);
499         } catch (IOException e) {
500             // Do nothing.
501         }
502         throw new ImmediateReturnException();
503     }
504 
505     /**
506      * Extract the XML document from the Base64 encoded assertion and verify the
507      * signature in the Base64 encoded signature.
508      * 
509      * @return the XML attribute assertion.
510      * @throws ImmediateReturnException
511      *             if we display an error page.
512      */
513     protected String extractAndVerifyUserInfo() throws ImmediateReturnException {
514         // Retrieve the assertion and decrypt it.
515         byte[] clearText = null;
516         try {
517             String base64CipherText = request.getParameter("assertion");
518             byte[] cipherText = Base64.decode(base64CipherText);
519             clearText = AESCrypt.decrypt(cipherText, filter.getShibShimServerCryptKey());
520         } catch (AESCryptException e) {
521             ErrorPage.displayErrorPage(response, "AES decrypt exception: " + e);
522         }
523 
524         // Get the signature and verify the assertion against it.
525         boolean status = false;
526         try {
527             String signature = request.getParameter("signature");
528             byte[] sigBlock = Base64.decode(signature);
529             status = RSASignature.verify(clearText, sigBlock, filter.getShibShimServerCert());
530         } catch (RSASignatureException e) {
531             ErrorPage.displayErrorPage(response, "RSA signature exception: " + e);
532         }
533 
534         if (!status) {
535             ErrorPage.displayErrorPage(response, "The Shibboleth Shim Server signature failed to verify");
536         }
537 
538         String assertion = new String(clearText);
539         log.debug("assertion = \"" + assertion + "\"");
540         return assertion;
541     }
542 
543     /**
544      * Get the value for an integer header in the configuration attributes.
545      * 
546      * @param attr
547      *            the attribute name.
548      * @param defaultValue
549      *            the value to return if the attribute is not present.
550      * @return the value of the attribute, or the defaultValue if not present.
551      * @throws ImmediateReturnException
552      *             if we print an error page because the value did not parse as
553      *             an integer.
554      */
555     private long getIntConfigHeader(String attr, long defaultValue) throws ImmediateReturnException {
556         long intValue = defaultValue;
557         String tmp = (String) attributes.get(attr);
558         if (tmp != null) {
559             try {
560                 intValue = Long.parseLong(tmp);
561             } catch (NumberFormatException e) {
562                 ErrorPage.displayErrorPage(response, e.getMessage());
563             }
564         }
565         return intValue;
566     }
567 
568     /**
569      * If necessary, create the attributes object and initialize it.
570      * 
571      * @param clearAttributes
572      *            set to <tt>true</tt> if the attributes from the session
573      *            object should be cleared.
574      * @throws ImmediateReturnException
575      *             if we print an error page.
576      */
577     private void initializeAttributes(boolean clearAttributes) throws ImmediateReturnException {
578         if (attributes == null) {
579             attributes = new UserAttributesImpl(filter.getAttributeMap());
580             if (attributes == null) {
581                 log.error("Cannot create " + filter.getSessionAttribute() + " object");
582                 ErrorPage.displayErrorPage(response, "Unable to log in due to a system problem on the application's server");
583             }
584             session.setAttribute(filter.getSessionAttribute(), attributes);
585         }
586 
587         if (clearAttributes) {
588             attributes.clearUserInfo();
589         }
590     }
591 }