Removed jsp attributes that the Handle Servlet was not passing to the error page.
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / shire / ShireServlet.java
1 /* 
2  * The Shibboleth License, Version 1. 
3  * Copyright (c) 2002 
4  * University Corporation for Advanced Internet Development, Inc. 
5  * All rights reserved
6  * 
7  * 
8  * Redistribution and use in source and binary forms, with or without 
9  * modification, are permitted provided that the following conditions are met:
10  * 
11  * Redistributions of source code must retain the above copyright notice, this 
12  * list of conditions and the following disclaimer.
13  * 
14  * Redistributions in binary form must reproduce the above copyright notice, 
15  * this list of conditions and the following disclaimer in the documentation 
16  * and/or other materials provided with the distribution, if any, must include 
17  * the following acknowledgment: "This product includes software developed by 
18  * the University Corporation for Advanced Internet Development 
19  * <http://www.ucaid.edu>Internet2 Project. Alternately, this acknowledegement 
20  * may appear in the software itself, if and wherever such third-party 
21  * acknowledgments normally appear.
22  * 
23  * Neither the name of Shibboleth nor the names of its contributors, nor 
24  * Internet2, nor the University Corporation for Advanced Internet Development, 
25  * Inc., nor UCAID may be used to endorse or promote products derived from this 
26  * software without specific prior written permission. For written permission, 
27  * please contact shibboleth@shibboleth.org
28  * 
29  * Products derived from this software may not be called Shibboleth, Internet2, 
30  * UCAID, or the University Corporation for Advanced Internet Development, nor 
31  * may Shibboleth appear in their name, without prior written permission of the 
32  * University Corporation for Advanced Internet Development.
33  * 
34  * 
35  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
36  * AND WITH ALL FAULTS. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
37  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 
38  * PARTICULAR PURPOSE, AND NON-INFRINGEMENT ARE DISCLAIMED AND THE ENTIRE RISK 
39  * OF SATISFACTORY QUALITY, PERFORMANCE, ACCURACY, AND EFFORT IS WITH LICENSEE. 
40  * IN NO EVENT SHALL THE COPYRIGHT OWNER, CONTRIBUTORS OR THE UNIVERSITY 
41  * CORPORATION FOR ADVANCED INTERNET DEVELOPMENT, INC. BE LIABLE FOR ANY DIRECT, 
42  * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 
43  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
44  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 
45  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 
46  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
47  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
48  */
49
50 package edu.internet2.middleware.shibboleth.shire;
51
52 import java.io.ByteArrayOutputStream;
53 import java.io.File;
54 import java.io.FileWriter;
55 import java.io.IOException;
56 import java.io.PrintWriter;
57 import java.security.Key;
58 import java.security.KeyStore;
59 import java.security.KeyStoreException;
60 import java.security.NoSuchAlgorithmException;
61 import java.security.cert.Certificate;
62 import java.security.cert.CertificateException;
63
64 import javax.servlet.RequestDispatcher;
65 import javax.servlet.ServletException;
66 import javax.servlet.UnavailableException;
67 import javax.servlet.http.Cookie;
68 import javax.servlet.http.HttpServlet;
69 import javax.servlet.http.HttpServletRequest;
70 import javax.servlet.http.HttpServletResponse;
71 import javax.servlet.http.HttpUtils;
72
73 import org.apache.log4j.Logger;
74 import org.doomdark.uuid.UUIDGenerator;
75 import org.opensaml.SAMLAuthenticationStatement;
76 import org.opensaml.SAMLAssertion;
77 import org.opensaml.SAMLException;
78 import org.opensaml.SAMLResponse;
79 import sun.misc.BASE64Decoder;
80
81 import edu.internet2.middleware.shibboleth.common.Constants;
82 import edu.internet2.middleware.shibboleth.common.OriginSiteMapperException;
83 import edu.internet2.middleware.shibboleth.common.ShibPOSTProfile;
84 import edu.internet2.middleware.shibboleth.common.ShibPOSTProfileFactory;
85
86 /**
87  *  Implements a SAML POST profile consumer
88  *
89  * @author     Scott Cantor
90  * @created    June 10, 2002
91  */
92
93 public class ShireServlet extends HttpServlet {
94
95         private String shireLocation;
96         private String cookieName;
97         private String cookieDomain;
98         private String sessionDir;
99         private String keyStorePath;
100         private String keyStorePasswd;
101         private String keyStoreAlias;
102         private String registryURI;
103         private boolean sslOnly = true;
104         private boolean checkAddress = true;
105         private boolean verbose = false;
106
107         private XMLOriginSiteMapper mapper = null;
108         private static Logger log = Logger.getLogger(ShireServlet.class.getName());
109
110         /**
111          *  Use the following servlet init parameters:<P>
112          *
113          *
114          *  <DL>
115          *    <DT> shire-location <I>(optional)</I> </DT>
116          *    <DD> The URL of the SHIRE if not derivable from requests</DD>
117          *    <DT> keystore-path <I>(required)</I> </DT>
118          *    <DD> A pathname to the trusted CA roots to accept</DD>
119          *    <DT> keystore-password <I>(required)</I> </DT>
120          *    <DD> The root keystore password</DD>
121          *    <DT> keystore-alias <I>(optional)</I> </DT>
122          *    <DD> An alias in the provided keystore for the cert that can verify
123          *    the origin site registry signature</DD>
124          *    <DT> registry-uri <I>(required)</I> </DT>
125          *    <DD> The origin site registry URI to install</DD>
126          *    <DT> cookie-name <I>(required)</I> </DT>
127          *    <DD> Name of session cookie to set in browser</DD>
128          *    <DT> cookie-domain <I>(optional)</I> </DT>
129          *    <DD> Domain of session cookie to set in browser</DD>
130          *    <DT> ssl-only <I>(defaults to true)</I> </DT>
131          *    <DD> If true, allow only SSL-protected POSTs and issue a secure cookie
132          *    </DD>
133          *    <DT> check-address <I>(defaults to true)</I> </DT>
134          *    <DD> If true, check client's IP address against assertion</DD>
135          *    <DT> session-dir <I>(defaults to /tmp)</I> </DT>
136          *    <DD> Directory in which to place session files</DD>
137          *  </DL>
138          *
139          */
140         public void init() throws ServletException {
141                 super.init();
142                 log.info("Initializing SHIRE.");
143
144                 edu.internet2.middleware.shibboleth.common.Init.init();
145
146                 loadInitParams();
147                 verifyConfig();
148
149                 log.info("Loading keystore.");
150                 try {
151                         Key k = null;
152                         KeyStore ks = KeyStore.getInstance("JKS");
153                         ks.load(getServletContext().getResourceAsStream(keyStorePath), keyStorePasswd.toCharArray());
154
155                         log.debug("Configured to use keystore-alias (" + keyStoreAlias + ") to verify site file.");
156                         if (keyStoreAlias != null) {
157                                 Certificate cert;
158                                 cert = ks.getCertificate(keyStoreAlias);
159
160                                 if (cert == null || (k = cert.getPublicKey()) == null) {
161                                         log.fatal(
162                                                 "Unable to load registry verification certificate ("
163                                                         + keyStoreAlias
164                                                         + ") from keystore");
165                                         throw new UnavailableException(
166                                                 "Unable to load registry verification certificate ("
167                                                         + keyStoreAlias
168                                                         + ") from keystore");
169                                 }
170                         }
171
172                         log.info("Loading shibboleth site information.");
173                         mapper = new XMLOriginSiteMapper(registryURI, k, ks);
174                         log.info("Completed SHIRE initialization");
175
176                 } catch (OriginSiteMapperException e) {
177                         log.fatal("Configuration problem: Unable load shibboleth site information." + e);
178                         throw new UnavailableException(
179                                 "Configuration problem: Unable load shibboleth site information." + e);
180                 } catch (KeyStoreException e) {
181                         log.fatal("Configuration problem: Unable to load supplied keystore." + e);
182                         throw new UnavailableException("Configuration problem: Unable load supplied keystore." + e);
183                 } catch (NoSuchAlgorithmException e) {
184                         log.fatal("Configuration problem: Unable to load supplied keystore." + e);
185                         throw new UnavailableException("Configuration problem: Unable load supplied keystore." + e);
186                 } catch (CertificateException e) {
187                         log.fatal("Configuration problem: Unable to load supplied keystore." + e);
188                         throw new UnavailableException("Configuration problem: Unable load supplied keystore." + e);
189                 } catch (IOException e) {
190                         log.fatal("Configuration problem: Unable to loadsupplied keystore." + e);
191                         throw new UnavailableException("Configuration problem: Unable load supplied keystore." + e);
192                 }
193
194         }
195
196         /**
197          * Ensures that all required initialization attributes have been set.
198          */
199         private void verifyConfig() throws UnavailableException {
200
201                 if (cookieName == null) {
202                         log.fatal("Init parameter (cookie-name) is required in deployment descriptor.");
203                         throw new UnavailableException("Init parameter (cookie-name) is required in deployment descriptor.");
204                 }
205
206                 if (registryURI == null) {
207                         log.fatal("Init parameter (registry-uri) is required in deployment descriptor.");
208                         throw new UnavailableException("Init parameter (registry-uri) is required in deployment descriptor.");
209                 }
210
211                 if (keyStorePath == null) {
212                         log.fatal("Init parameter (keystore-path) is required in deployment descriptor.");
213                         throw new UnavailableException("Init parameter (keystore-path) is required in deployment descriptor.");
214                 }
215
216                 if (keyStorePasswd == null) {
217                         log.fatal("Init parameter (keystore-password) is required in deployment descriptor.");
218                         throw new UnavailableException("Init parameter (keystore-password) is required in deployment descriptor.");
219                 }
220
221         }
222
223         /**
224          * Loads SHIRE configuration parameters.  Sets default values as appropriate.
225          */
226         private void loadInitParams() {
227
228                 log.info("Loading configuration from deployment descriptor (web.xml).");
229
230                 shireLocation = getServletConfig().getInitParameter("shire-location");
231                 cookieDomain = getServletConfig().getInitParameter("cookie-domain");
232                 cookieName = getServletConfig().getInitParameter("cookie-name");
233                 keyStorePath = getServletConfig().getInitParameter("keystore-path");
234                 keyStorePasswd = getServletConfig().getInitParameter("keystore-password");
235                 keyStoreAlias = getServletConfig().getInitParameter("keystore-alias");
236                 registryURI = getServletConfig().getInitParameter("registry-uri");
237
238                 sessionDir = getServletConfig().getInitParameter("session-dir");
239                 if (sessionDir == null) {
240                         sessionDir = "/tmp";
241                         log.warn("No session-dir parameter found... using default location: (" + sessionDir + ").");
242                 }
243
244                 String temp = getServletConfig().getInitParameter("ssl-only");
245                 if (temp != null && (temp.equalsIgnoreCase("false") || temp.equals("0")))
246                         sslOnly = false;
247
248                 temp = getServletConfig().getInitParameter("check-address");
249                 if (temp != null && (temp.equalsIgnoreCase("false") || temp.equals("0")))
250                         checkAddress = false;
251
252         }
253
254         /**
255          *  Processes a sign-on submission<P>
256          *
257          *
258          *
259          * @param  request               HTTP request context
260          * @param  response              HTTP response context
261          * @exception  IOException       Thrown if an I/O error occurs
262          * @exception  ServletException  Thrown if a servlet engine error occurs
263          */
264         public void doPost(HttpServletRequest request, HttpServletResponse response)
265                 throws IOException, ServletException {
266
267                 try {
268
269                         log.info("Received a handle package.");
270                         log.debug("Target URL from client: " + request.getParameter("TARGET"));
271                         validateRequest(request);
272
273                         SAMLAuthenticationStatement s = processAssertion(request,response);
274                         if (s==null)
275                             return;
276                         shareSession(
277                                 response,
278                                 s.getSubject().getName(),
279                                 s.getSubject().getNameQualifier(),
280                                 System.currentTimeMillis(),
281                                 request.getRemoteAddr(),
282                                 s.getBindings()[0].getBinding(),
283                                 s.getBindings()[0].getLocation());
284
285                         log.info("Redirecting to the requested resource: (" + request.getParameter("TARGET") + ").");
286                         response.sendRedirect(request.getParameter("TARGET"));
287
288                 } catch (ShireException se) {
289                         handleError(se, request, response);
290                 }
291
292         }
293
294         /**
295          * Extracts a SAML Authentication Assertion from a POST request object and performs appropriate validity 
296          * checks on the same. 
297          *
298          * @param  request The <code>HttpServletRequest</code> object for the current request
299          * @param  response The <code>HttpServletResponse</code> object for the current request
300          * @exception  ShireException  Thrown if any error is encountered parsing or validating the assertion 
301          * that is retreived from the request object.
302          */
303
304         private SAMLAuthenticationStatement processAssertion(HttpServletRequest request, HttpServletResponse response) throws ShireException {
305
306                 log.info("Processing SAML Assertion.");
307                 try {
308                         // Get a profile object using our specifics.
309                         String[] policies = { Constants.POLICY_CLUBSHIB };
310                         ShibPOSTProfile profile =
311                                 ShibPOSTProfileFactory.getInstance(
312                                         policies,
313                                         mapper,
314                                         (shireLocation != null) ? shireLocation : HttpUtils.getRequestURL(request).toString(),
315                                         300);
316
317                         if (log.isDebugEnabled()) {
318                                 try {
319                                         log.debug(
320                                                 "Dumping unparsed SAML Response:"
321                                                         + System.getProperty("line.separator")
322                                                         + new String(
323                                                                 new BASE64Decoder().decodeBuffer(request.getParameter("SAMLResponse")),
324                                                                 "UTF8"));
325                                 } catch (IOException e) {
326                                         log.error("Encountered an error while decoding SAMLReponse for loggin purposes.");
327                                 }
328                         }
329
330                         // Try and accept the response...
331                         SAMLResponse r = profile.accept(request.getParameter("SAMLResponse").getBytes());
332
333                         // We've got a valid signed response we can trust (or the whole response was empty...)
334
335                         if (log.isDebugEnabled()) {
336                             ByteArrayOutputStream bytestr = new ByteArrayOutputStream();
337                             try {
338                                 r.toStream(bytestr);
339                             } catch (IOException e) {
340                                 log.error("Very Strange... problem converting SAMLResponse to a Stream for logging purposes.");
341                             }
342
343                             log.debug(
344                                 "Dumping parsed SAML Response:" + System.getProperty("line.separator") + bytestr.toString());
345                         }
346
347                         // Get the assertion we need.
348                         SAMLAssertion a = profile.getSSOAssertion(r);
349                         if (a == null) {
350                                 throw new ShireException("The assertion of your Shibboleth identity was missing or incompatible with the policies of this site.");
351                         }
352
353                         // Check for replay, in which case we just redirect to the target.
354
355                         if (!profile.checkReplayCache(a)) {
356                             log.debug("Detected a replayed assertion, forwarding to target");
357                             try {
358                                 response.sendRedirect(request.getParameter("TARGET"));
359                                 return null;
360                             } catch (IOException e) {
361                                 throw new ShireException("Unable to redirect browser to target after detecting replay.");
362                             }
363                         }
364
365                         // Get the statement we need.
366                         SAMLAuthenticationStatement s = profile.getSSOStatement(a);
367                         if (s == null) {
368                                 throw new ShireException("The assertion of your Shibboleth identity was missing or incompatible with the policies of this site.");
369                         }
370
371                         if (checkAddress) {
372                                 log.debug("Running with client address checking enabled.");
373                                 log.debug("Client Address from request: " + request.getRemoteAddr());
374                                 log.debug("Client Address from assertion: " + s.getSubjectIP());
375                                 if (s.getSubjectIP() == null || !s.getSubjectIP().equals(request.getRemoteAddr())) {
376                                         throw new ShireException("The IP address provided by your origin site was either missing or did not match your current address.  To correct this problem, you may need to bypass a local proxy server.");
377                                 }
378                         } else {
379                                 log.debug("Running with client address checking disabled.");
380                         }
381
382                         // All we really need is here...
383                         log.debug("Shibboleth Origin Site: " + s.getSubject().getNameQualifier());
384                         log.debug("Shibboleth Handle: " + s.getSubject().getName());
385                         log.debug("Shibboleth AA URL: " + s.getBindings()[0].getLocation());
386                         return s;
387
388                 } catch (SAMLException e) {
389                         throw new ShireException("Error processing SAML assertion: " + e);
390                 }
391         }
392
393         /**
394          * Makes user information available to SHAR.
395          * 
396          */
397
398         private void shareSession(
399                 HttpServletResponse response,
400                 String handle,
401                 String domain,
402                 long currentTime,
403                 String clientAddress,
404                 String protocolBinding,
405                 String locationBinding)
406                 throws ShireException {
407
408                 log.info("Generating SHIR/SHAR shared data.");
409                 String filename = UUIDGenerator.getInstance().generateRandomBasedUUID().toString();
410                 log.debug("Created unique session identifier: " + filename);
411
412                 // Write session identifier to a file           
413                 String pathname = null;
414                 if (sessionDir.endsWith(File.separator))
415                         pathname = sessionDir + filename;
416                 else
417                         pathname = sessionDir + File.separatorChar + filename;
418                 PrintWriter fout;
419                 try {
420                         log.debug("Writing session data to file: (" + pathname + ")");
421                         fout = new PrintWriter(new FileWriter(pathname));
422
423                         log.debug("Session Pathname: " + pathname);
424
425                         fout.println("Handle=" + handle);
426                         fout.println("Domain=" + domain);
427                         fout.println("PBinding0=" + protocolBinding);
428                         fout.println("LBinding0=" + locationBinding);
429                         fout.println("Time=" + currentTime / 1000);
430                         fout.println("ClientAddress=" + clientAddress);
431                         fout.println("EOF");
432                         fout.close();
433
434                         Cookie cookie = new Cookie(cookieName, filename);
435                         cookie.setPath("/");
436                         if (cookieDomain != null)
437                                 cookie.setDomain(cookieDomain);
438                         log.debug(
439                                 "Adding session identifier to browser cookie: ("
440                                         + cookie.getDomain()
441                                         + ":"
442                                         + cookie.getName()
443                                         + ")");
444                         response.addCookie(cookie);
445
446                 } catch (IOException e) {
447                         log.error("Unable to write session to file (" + filename + ") : " + e);
448                         throw new ShireException("Unable to share session with SHAR.");
449                 }
450         }
451
452         /**
453          * Ensures that the POST request contains the necessary data elements
454          * 
455          * @param request <code>The HttpServletRequest</code> object for the current request
456          * @exception ShireException thrown if required POST data is missing
457          */
458
459         private void validateRequest(HttpServletRequest request) throws ShireException {
460
461                 log.info("Validating POST request properties.");
462
463                 if (sslOnly && !request.isSecure()) {
464                         throw new ShireException("Access to this site requires the use of SSL.");
465                 }
466
467                 if (request.getParameter("TARGET") == null || request.getParameter("TARGET").length() == 0) {
468                         throw new ShireException("Invalid data from HS: No target URL received.");
469                 }
470
471                 if (request.getParameter("SAMLResponse") == null
472                         || request.getParameter("SAMLResponse").length() == 0) {
473                         throw new ShireException("Invalid data from HS: No SAML Assertion included received.");
474                 }
475
476         }
477
478         /**
479          * Appropriately routes all recoverable errors encountered by the SHIRE
480          */
481
482         private void handleError(ShireException se, HttpServletRequest req, HttpServletResponse res) {
483                 log.error(se);
484                 log.debug("Displaying error page.");
485                 req.setAttribute("errorText", se.toString());
486                 req.setAttribute("requestURL", req.getRequestURI().toString());
487                 RequestDispatcher rd = req.getRequestDispatcher("/shireerror.jsp");
488
489                 try {
490                         rd.forward(req, res);
491                 } catch (IOException ioe) {
492                         log.error("Problem trying to display SHIRE error page: " + ioe.toString());
493                 } catch (ServletException servletE) {
494                         log.error("Problem trying to display SHIRE error page: " + servletE.toString());
495                 }
496         }
497 }